[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (fe31400 -> 6144c4a)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (fe31400 -> 6144c4a) |
Date: |
Wed, 06 Oct 2021 16:17:51 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a change to branch master
in repository libeufin.
from fe31400 demobank notes and skeleton
new ffef8c7 Unix domain socket server.
new 3ab168c Unix domain sockets dependencies
new 3552c78 adapt test to new API
new e1c7fdf test for Unix domain socket
new 2b788ab test dep
new b4942a1 Unix domain socket proxy: fix payload read/write.
new 6144c4a Serve Sandbox via Unix domain socket.
The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../src/main/kotlin/tech/libeufin/sandbox/Main.kt | 1248 ++++++++++----------
sandbox/src/test/kotlin/CamtTest.kt | 34 -
sandbox/src/test/kotlin/DBTest.kt | 2 +-
util/build.gradle | 6 +
util/src/main/kotlin/UnixDomainSocket.kt | 97 ++
util/src/test/kotlin/DomainSocketTest.kt | 30 +
6 files changed, 763 insertions(+), 654 deletions(-)
delete mode 100644 sandbox/src/test/kotlin/CamtTest.kt
create mode 100644 util/src/main/kotlin/UnixDomainSocket.kt
create mode 100644 util/src/test/kotlin/DomainSocketTest.kt
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index f70f7ed..fbde053 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -38,35 +38,21 @@ package tech.libeufin.sandbox
import UtilError
import com.fasterxml.jackson.core.JsonParseException
-import io.ktor.application.ApplicationCallPipeline
-import io.ktor.application.call
-import io.ktor.application.install
-import io.ktor.features.CallLogging
-import io.ktor.features.ContentNegotiation
-import io.ktor.features.StatusPages
-import io.ktor.response.respond
-import io.ktor.response.respondText
import io.ktor.server.engine.embeddedServer
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
-import io.ktor.jackson.jackson
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import org.slf4j.event.Level
import org.w3c.dom.Document
+import io.ktor.jackson.*
import tech.libeufin.util.CryptoUtil
import tech.libeufin.util.RawPayment
import java.lang.ArithmeticException
import java.math.BigDecimal
import java.security.interfaces.RSAPublicKey
import javax.xml.bind.JAXBContext
-import com.fasterxml.jackson.core.util.DefaultIndenter
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-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 org.jetbrains.exposed.sql.statements.api.ExposedBlob
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.core.context
@@ -77,21 +63,33 @@ import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.int
import execThrowableOrTerminate
import io.ktor.application.ApplicationCall
+import io.ktor.application.ApplicationCallPipeline
+import io.ktor.application.call
+import io.ktor.application.install
+import io.ktor.features.CallLogging
+import io.ktor.features.ContentNegotiation
+import org.jetbrains.exposed.sql.statements.api.ExposedBlob
+import com.fasterxml.jackson.core.util.DefaultIndenter
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.databind.SerializationFeature
+import
org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
+import io.ktor.features.StatusPages
+import io.ktor.response.respond
+import io.ktor.response.respondText
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.server.netty.*
import io.ktor.util.date.*
+import io.ktor.application.*
import kotlinx.coroutines.newSingleThreadContext
-import
org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
+import startServer
import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.EbicsResponse
-import tech.libeufin.util.ebics_h004.EbicsTypes
import validatePlainAmount
import java.net.BindException
import java.util.*
-import kotlin.random.Random
import kotlin.system.exitProcess
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
@@ -290,9 +288,22 @@ class Serve : CliktCommand("Run sandbox HTTP server") {
private val logLevel by option()
private val port by option().int().default(5000)
+ private val withUnixSocket by option(
+ help = "Bind the Sandbox to the Unix domain socket at PATH.
Overrides" +
+ "--port, when both are given", metavar = "PATH"
+ )
override fun run() {
setLogLevel(logLevel)
- serverMain(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME), port)
+ val dbName = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
+ if (withUnixSocket != null) {
+ execThrowableOrTerminate { dbCreateTables(dbName) }
+ startServer(
+ withUnixSocket ?: throw Exception("Could not use the Unix
domain socket path value!"),
+ app = sandboxApp
+ )
+ exitProcess(0)
+ }
+ serverMain(dbName, port)
}
}
@@ -384,703 +395,702 @@ suspend inline fun <reified T : Any>
ApplicationCall.receiveJson(): T {
}
val singleThreadContext = newSingleThreadContext("DB")
-
-fun serverMain(dbName: String, port: Int) {
- execThrowableOrTerminate { dbCreateTables(dbName) }
- val myLogger = logger
- val server = embeddedServer(Netty, port = port) {
- install(CallLogging) {
- this.level = Level.DEBUG
- this.logger = myLogger
- }
- install(Authentication) {
- // Web-based authentication for Bank customers.
- form("auth-form") {
- userParamName = "username"
- passwordParamName = "password"
- validate { credentials ->
- if (credentials.name == "test") {
- UserIdPrincipal(credentials.name)
- } else {
- null
- }
+val sandboxApp: Application.() -> Unit = {
+ install(io.ktor.features.CallLogging) {
+ this.level = org.slf4j.event.Level.DEBUG
+ this.logger = logger
+ }
+ install(Authentication) {
+ // Web-based authentication for Bank customers.
+ form("auth-form") {
+ userParamName = "username"
+ passwordParamName = "password"
+ validate { credentials ->
+ if (credentials.name == "test") {
+ UserIdPrincipal(credentials.name)
+ } else {
+ null
}
}
}
- install(ContentNegotiation) {
- jackson {
- enable(SerializationFeature.INDENT_OUTPUT)
- setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
-
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
- indentObjectsWith(DefaultIndenter(" ", "\n"))
- })
- registerModule(KotlinModule(nullisSameAsDefault = true))
- //registerModule(JavaTimeModule())
- }
+ }
+ install(io.ktor.features.ContentNegotiation) {
+ jackson {
+
enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT)
+ setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
+
indentArraysWith(com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter.instance)
+ indentObjectsWith(DefaultIndenter(" ", "\n"))
+ })
+ registerModule(KotlinModule(nullisSameAsDefault = true))
+ //registerModule(JavaTimeModule())
}
- install(StatusPages) {
- exception<ArithmeticException> { cause ->
- logger.error("Exception while handling '${call.request.uri}'",
cause)
- call.respondText(
- "Invalid arithmetic attempted.",
- ContentType.Text.Plain,
- // here is always the bank's fault, as it should always
check
- // the operands.
- HttpStatusCode.InternalServerError
+ }
+ install(StatusPages) {
+ exception<ArithmeticException> { cause ->
+ logger.error("Exception while handling '${call.request.uri}'",
cause)
+ call.respondText(
+ "Invalid arithmetic attempted.",
+ io.ktor.http.ContentType.Text.Plain,
+ // here is always the bank's fault, as it should always check
+ // the operands.
+ io.ktor.http.HttpStatusCode.InternalServerError
+ )
+ }
+ exception<EbicsRequestError> { cause ->
+ val resp =
tech.libeufin.util.ebics_h004.EbicsResponse.createForUploadWithError(
+ cause.errorText,
+ cause.errorCode,
+ // assuming that the phase is always transfer,
+ // as errors during initialization should have
+ // already been caught by the chunking logic.
+
tech.libeufin.util.ebics_h004.EbicsTypes.TransactionPhaseType.TRANSFER
+ )
+
+ val hostAuthPriv = transaction {
+ val host = tech.libeufin.sandbox.EbicsHostEntity.find {
+ tech.libeufin.sandbox.EbicsHostsTable.hostID.upperCase()
eq call.attributes.get(tech.libeufin.sandbox.EbicsHostIdAttribute).uppercase()
+ }.firstOrNull() ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.InternalServerError,
+ "Requested Ebics host ID not found."
)
+
tech.libeufin.util.CryptoUtil.loadRsaPrivateKey(host.authenticationPrivateKey.bytes)
}
- exception<EbicsRequestError> { cause ->
- val resp = EbicsResponse.createForUploadWithError(
- cause.errorText,
- cause.errorCode,
- // assuming that the phase is always transfer,
- // as errors during initialization should have
- // already been caught by the chunking logic.
- EbicsTypes.TransactionPhaseType.TRANSFER
- )
-
- val hostAuthPriv = transaction {
- val host = EbicsHostEntity.find {
- EbicsHostsTable.hostID.upperCase() eq
call.attributes.get(EbicsHostIdAttribute).uppercase()
- }.firstOrNull() ?: throw SandboxError(
- HttpStatusCode.InternalServerError,
- "Requested Ebics host ID not found."
+ call.respondText(
+ tech.libeufin.util.XMLUtil.signEbicsResponse(resp,
hostAuthPriv),
+ io.ktor.http.ContentType.Application.Xml,
+ io.ktor.http.HttpStatusCode.OK
+ )
+ }
+ exception<SandboxError> { cause ->
+ logger.error("Exception while handling '${call.request.uri}',
${cause.reason}")
+ call.respond(
+ cause.statusCode,
+ SandboxErrorJson(
+ error = SandboxErrorDetailJson(
+ type = "sandbox-error",
+ description = cause.reason
)
-
CryptoUtil.loadRsaPrivateKey(host.authenticationPrivateKey.bytes)
- }
- call.respondText(
- XMLUtil.signEbicsResponse(resp, hostAuthPriv),
- ContentType.Application.Xml,
- HttpStatusCode.OK
)
- }
- exception<SandboxError> { cause ->
- logger.error("Exception while handling '${call.request.uri}',
${cause.reason}")
- call.respond(
- cause.statusCode,
- SandboxErrorJson(
- error = SandboxErrorDetailJson(
- type = "sandbox-error",
- description = cause.reason
- )
+ )
+ }
+ exception<UtilError> { cause ->
+ logger.error("Exception while handling '${call.request.uri}'",
cause)
+ call.respond(
+ cause.statusCode,
+ SandboxErrorJson(
+ error = SandboxErrorDetailJson(
+ type = "util-error",
+ description = cause.reason
)
)
- }
- exception<UtilError> { cause ->
- logger.error("Exception while handling '${call.request.uri}'",
cause)
- call.respond(
- cause.statusCode,
- SandboxErrorJson(
- error = SandboxErrorDetailJson(
- type = "util-error",
- description = cause.reason
- )
- )
+ )
+ }
+ exception<Throwable> { cause ->
+ logger.error("Exception while handling '${call.request.uri}'",
cause)
+ call.respondText("Internal server error.",
io.ktor.http.ContentType.Text.Plain,
io.ktor.http.HttpStatusCode.InternalServerError)
+ }
+ }
+ intercept(io.ktor.application.ApplicationCallPipeline.Fallback) {
+ if (this.call.response.status() == null) {
+ call.respondText("Not found (no route matched).\n",
io.ktor.http.ContentType.Text.Plain, io.ktor.http.HttpStatusCode.NotFound)
+ return@intercept finish()
+ }
+ }
+ routing {
+
+ get("/") {
+ call.respondText("Hello, this is Sandbox\n",
io.ktor.http.ContentType.Text.Plain)
+ }
+ get("/config") {
+ call.respond(object {
+ val name = "libeufin-sandbox"
+
+ // FIXME: use actual version here!
+ val version = "0.0.0-dev.0"
+ })
+ }
+ /**
+ * For now, only returns the last statement of the
+ * requesting account.
+ */
+ post("/admin/payments/camt") {
+ requireSuperuser(call.request)
+ val body = call.receiveJson<CamtParams>()
+ val bankaccount = getAccountFromLabel(body.bankaccount)
+ if (body.type != 53) throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound,
+ "Only Camt.053 documents can be generated."
+ )
+ val camtMessage = transaction {
+ tech.libeufin.sandbox.BankAccountStatementEntity.find {
+
tech.libeufin.sandbox.BankAccountStatementsTable.bankAccount eq bankaccount.id
+ }.lastOrNull()?.xmlMessage ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound,
+ "Could not find any statements; please wait next tick"
)
}
- exception<Throwable> { cause ->
- logger.error("Exception while handling '${call.request.uri}'",
cause)
- call.respondText("Internal server error.",
ContentType.Text.Plain, HttpStatusCode.InternalServerError)
- }
+ call.respondText(
+ camtMessage, io.ktor.http.ContentType.Text.Xml,
io.ktor.http.HttpStatusCode.OK
+ )
+ return@post
}
- intercept(ApplicationCallPipeline.Fallback) {
- if (this.call.response.status() == null) {
- call.respondText("Not found (no route matched).\n",
ContentType.Text.Plain, HttpStatusCode.NotFound)
- return@intercept finish()
+
+ post("/admin/bank-accounts/{label}") {
+ requireSuperuser(call.request)
+ val body = call.receiveJson<BankAccountInfo>()
+ transaction {
+ tech.libeufin.sandbox.BankAccountEntity.new {
+ iban = body.iban
+ bic = body.bic
+ name = body.name
+ label = body.label
+ currency = body.currency ?: "EUR"
+ }
}
+ call.respond(object {})
+ return@post
}
- routing {
- get("/") {
- call.respondText("Hello, this is Sandbox\n",
ContentType.Text.Plain)
+ get("/admin/bank-accounts/{label}") {
+ requireSuperuser(call.request)
+ val label = ensureNonNull(call.parameters["label"])
+ val ret = transaction {
+ val bankAccount = tech.libeufin.sandbox.BankAccountEntity.find
{
+ tech.libeufin.sandbox.BankAccountsTable.label eq label
+ }.firstOrNull() ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound,
+ "Account '$label' not found"
+ )
+ val balance = balanceForAccount(bankAccount)
+ object {
+ val balance = "${bankAccount.currency}:${balance}"
+ val iban = bankAccount.iban
+ val bic = bankAccount.bic
+ val name = bankAccount.name
+ val label = bankAccount.label
+ }
}
- get("/config") {
- call.respond(object {
- val name = "libeufin-sandbox"
+ call.respond(ret)
+ return@get
+ }
- // FIXME: use actual version here!
- val version = "0.0.0-dev.0"
- })
+ post("/admin/bank-accounts/{label}/simulate-incoming-transaction") {
+ requireSuperuser(call.request)
+ val body = call.receiveJson<IncomingPaymentInfo>()
+ // FIXME: generate nicer UUID!
+ val accountLabel = ensureNonNull(call.parameters["label"])
+ if (!validatePlainAmount(body.amount)) {
+ throw SandboxError(
+ io.ktor.http.HttpStatusCode.BadRequest,
+ "invalid amount (should be plain amount without currency)"
+ )
}
- /**
- * For now, only returns the last statement of the
- * requesting account.
- */
- post("/admin/payments/camt") {
- requireSuperuser(call.request)
- val body = call.receiveJson<CamtParams>()
- val bankaccount = getAccountFromLabel(body.bankaccount)
- if (body.type != 53) throw SandboxError(
- HttpStatusCode.NotFound,
- "Only Camt.053 documents can be generated."
+ val reqDebtorBic = body.debtorBic
+ if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
+ throw SandboxError(
+ io.ktor.http.HttpStatusCode.BadRequest,
+ "invalid BIC"
)
- val camtMessage = transaction {
- BankAccountStatementEntity.find {
- BankAccountStatementsTable.bankAccount eq
bankaccount.id
- }.lastOrNull()?.xmlMessage ?: throw SandboxError(
- HttpStatusCode.NotFound,
- "Could not find any statements; please wait next tick"
- )
+ }
+ transaction {
+ val account = getBankAccountFromLabel(accountLabel)
+ val randId = getRandomString(16)
+ tech.libeufin.sandbox.BankAccountTransactionEntity.new {
+ creditorIban = account.iban
+ creditorBic = account.bic
+ creditorName = account.name
+ debtorIban = body.debtorIban
+ debtorBic = reqDebtorBic
+ debtorName = body.debtorName
+ subject = body.subject
+ amount = body.amount
+ currency = account.currency
+ date = getUTCnow().toInstant().toEpochMilli()
+ accountServicerReference = "sandbox-$randId"
+ this.account = account
+ direction = "CRDT"
}
- call.respondText(
- camtMessage, ContentType.Text.Xml, HttpStatusCode.OK
- )
- return@post
}
+ call.respond(object {})
+ }
- post("/admin/bank-accounts/{label}") {
- requireSuperuser(call.request)
- val body = call.receiveJson<BankAccountInfo>()
- transaction {
- BankAccountEntity.new {
- iban = body.iban
- bic = body.bic
- name = body.name
- label = body.label
- currency = body.currency ?: "EUR"
- }
+ /**
+ * Associates a new bank account with an existing Ebics subscriber.
+ */
+ post("/admin/ebics/bank-accounts") {
+ requireSuperuser(call.request)
+ val body = call.receiveJson<BankAccountRequest>()
+ if (!validateBic(body.bic)) {
+ throw SandboxError(io.ktor.http.HttpStatusCode.BadRequest,
"invalid BIC (${body.bic})")
+ }
+ transaction {
+ val subscriber = getEbicsSubscriberFromDetails(
+ body.subscriber.userID,
+ body.subscriber.partnerID,
+ body.subscriber.hostID
+ )
+ val check = tech.libeufin.sandbox.BankAccountEntity.find {
+ tech.libeufin.sandbox.BankAccountsTable.iban eq body.iban
or (tech.libeufin.sandbox.BankAccountsTable.label eq body.label)
+ }.count()
+ if (check > 0) throw SandboxError(
+ io.ktor.http.HttpStatusCode.BadRequest,
+ "Either IBAN or account label were already taken; please
choose fresh ones"
+ )
+ subscriber.bankAccount =
tech.libeufin.sandbox.BankAccountEntity.new {
+ iban = body.iban
+ bic = body.bic
+ name = body.name
+ label = body.label
+ currency = body.currency.uppercase(java.util.Locale.ROOT)
}
- call.respond(object {})
- return@post
}
-
- get("/admin/bank-accounts/{label}") {
- requireSuperuser(call.request)
- val label = ensureNonNull(call.parameters["label"])
- val ret = transaction {
- val bankAccount = BankAccountEntity.find {
- BankAccountsTable.label eq label
- }.firstOrNull() ?: throw SandboxError(
- HttpStatusCode.NotFound,
- "Account '$label' not found"
+ call.respondText("Bank account created")
+ return@post
+ }
+ get("/admin/bank-accounts") {
+ requireSuperuser(call.request)
+ val accounts = mutableListOf<BankAccountInfo>()
+ transaction {
+ tech.libeufin.sandbox.BankAccountEntity.all().forEach {
+ accounts.add(
+ BankAccountInfo(
+ label = it.label,
+ name = it.name,
+ bic = it.bic,
+ iban = it.iban,
+ currency = it.currency
+ )
)
- val balance = balanceForAccount(bankAccount)
- object {
- val balance = "${bankAccount.currency}:${balance}"
- val iban = bankAccount.iban
- val bic = bankAccount.bic
- val name = bankAccount.name
- val label = bankAccount.label
- }
}
- call.respond(ret)
- return@get
}
-
- post("/admin/bank-accounts/{label}/simulate-incoming-transaction")
{
- requireSuperuser(call.request)
- val body = call.receiveJson<IncomingPaymentInfo>()
- // FIXME: generate nicer UUID!
+ call.respond(accounts)
+ }
+ get("/admin/bank-accounts/{label}/transactions") {
+ requireSuperuser(call.request)
+ val ret = AccountTransactions()
+ transaction {
val accountLabel = ensureNonNull(call.parameters["label"])
- if (!validatePlainAmount(body.amount)) {
- throw SandboxError(
- HttpStatusCode.BadRequest,
- "invalid amount (should be plain amount without
currency)"
- )
- }
- val reqDebtorBic = body.debtorBic
- if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
- throw SandboxError(
- HttpStatusCode.BadRequest,
- "invalid BIC"
- )
- }
transaction {
val account = getBankAccountFromLabel(accountLabel)
- val randId = getRandomString(16)
- BankAccountTransactionEntity.new {
+ tech.libeufin.sandbox.BankAccountTransactionEntity.find {
+
tech.libeufin.sandbox.BankAccountTransactionsTable.account eq account.id
+ }.forEach {
+ ret.payments.add(
+ PaymentInfo(
+ accountLabel = account.label,
+ creditorIban = it.creditorIban,
+ // FIXME: We need to modify the transactions
table to have an actual
+ // account servicer reference here.
+ accountServicerReference =
it.accountServicerReference,
+ paymentInformationId = it.pmtInfId,
+ debtorIban = it.debtorIban,
+ subject = it.subject,
+ date = GMTDate(it.date).toHttpDate(),
+ amount = it.amount,
+ creditorBic = it.creditorBic,
+ creditorName = it.creditorName,
+ debtorBic = it.debtorBic,
+ debtorName = it.debtorName,
+ currency = it.currency,
+ creditDebitIndicator = when (it.direction) {
+ "CRDT" -> "credit"
+ "DBIT" -> "debit"
+ else -> throw Error("invalid direction")
+ }
+ )
+ )
+ }
+ }
+ }
+ call.respond(ret)
+ }
+ post("/admin/bank-accounts/{label}/generate-transactions") {
+ requireSuperuser(call.request)
+ transaction {
+ val accountLabel = ensureNonNull(call.parameters["label"])
+ val account = getBankAccountFromLabel(accountLabel)
+ val transactionReferenceCrdt = getRandomString(8)
+ val transactionReferenceDbit = getRandomString(8)
+
+ run {
+ val amount = kotlin.random.Random.nextLong(5, 25)
+ tech.libeufin.sandbox.BankAccountTransactionEntity.new {
creditorIban = account.iban
creditorBic = account.bic
creditorName = account.name
- debtorIban = body.debtorIban
- debtorBic = reqDebtorBic
- debtorName = body.debtorName
- subject = body.subject
- amount = body.amount
+ debtorIban = "DE64500105178797276788"
+ debtorBic = "DEUTDEBB101"
+ debtorName = "Max Mustermann"
+ subject = "sample transaction
$transactionReferenceCrdt"
+ this.amount = amount.toString()
currency = account.currency
date = getUTCnow().toInstant().toEpochMilli()
- accountServicerReference = "sandbox-$randId"
+ accountServicerReference = transactionReferenceCrdt
this.account = account
direction = "CRDT"
}
}
- call.respond(object {})
- }
- /**
- * Associates a new bank account with an existing Ebics subscriber.
- */
- post("/admin/ebics/bank-accounts") {
- requireSuperuser(call.request)
- val body = call.receiveJson<BankAccountRequest>()
- if (!validateBic(body.bic)) {
- throw SandboxError(HttpStatusCode.BadRequest, "invalid BIC
(${body.bic})")
- }
- transaction {
- val subscriber = getEbicsSubscriberFromDetails(
- body.subscriber.userID,
- body.subscriber.partnerID,
- body.subscriber.hostID
- )
- val check = BankAccountEntity.find {
- BankAccountsTable.iban eq body.iban or
(BankAccountsTable.label eq body.label)
- }.count()
- if (check > 0) throw SandboxError(
- HttpStatusCode.BadRequest,
- "Either IBAN or account label were already taken;
please choose fresh ones"
- )
- subscriber.bankAccount = BankAccountEntity.new {
- iban = body.iban
- bic = body.bic
- name = body.name
- label = body.label
- currency = body.currency.uppercase(Locale.ROOT)
- }
- }
- call.respondText("Bank account created")
- return@post
- }
- get("/admin/bank-accounts") {
- requireSuperuser(call.request)
- val accounts = mutableListOf<BankAccountInfo>()
- transaction {
- BankAccountEntity.all().forEach {
- accounts.add(
- BankAccountInfo(
- label = it.label,
- name = it.name,
- bic = it.bic,
- iban = it.iban,
- currency = it.currency
- )
- )
- }
- }
- call.respond(accounts)
- }
- get("/admin/bank-accounts/{label}/transactions") {
- requireSuperuser(call.request)
- val ret = AccountTransactions()
- transaction {
- val accountLabel = ensureNonNull(call.parameters["label"])
- transaction {
- val account = getBankAccountFromLabel(accountLabel)
- BankAccountTransactionEntity.find {
- BankAccountTransactionsTable.account eq account.id
- }.forEach {
- ret.payments.add(
- PaymentInfo(
- accountLabel = account.label,
- creditorIban = it.creditorIban,
- // FIXME: We need to modify the
transactions table to have an actual
- // account servicer reference here.
- accountServicerReference =
it.accountServicerReference,
- paymentInformationId = it.pmtInfId,
- debtorIban = it.debtorIban,
- subject = it.subject,
- date = GMTDate(it.date).toHttpDate(),
- amount = it.amount,
- creditorBic = it.creditorBic,
- creditorName = it.creditorName,
- debtorBic = it.debtorBic,
- debtorName = it.debtorName,
- currency = it.currency,
- creditDebitIndicator = when (it.direction)
{
- "CRDT" -> "credit"
- "DBIT" -> "debit"
- else -> throw Error("invalid
direction")
- }
- )
- )
- }
- }
- }
- call.respond(ret)
- }
- post("/admin/bank-accounts/{label}/generate-transactions") {
- requireSuperuser(call.request)
- transaction {
- val accountLabel = ensureNonNull(call.parameters["label"])
- val account = getBankAccountFromLabel(accountLabel)
- val transactionReferenceCrdt = getRandomString(8)
- val transactionReferenceDbit = getRandomString(8)
-
- run {
- val amount = Random.nextLong(5, 25)
- BankAccountTransactionEntity.new {
- creditorIban = account.iban
- creditorBic = account.bic
- creditorName = account.name
- debtorIban = "DE64500105178797276788"
- debtorBic = "DEUTDEBB101"
- debtorName = "Max Mustermann"
- subject = "sample transaction
$transactionReferenceCrdt"
- this.amount = amount.toString()
- currency = account.currency
- date = getUTCnow().toInstant().toEpochMilli()
- accountServicerReference = transactionReferenceCrdt
- this.account = account
- direction = "CRDT"
- }
- }
-
- run {
- val amount = Random.nextLong(5, 25)
-
- BankAccountTransactionEntity.new {
- debtorIban = account.iban
- debtorBic = account.bic
- debtorName = account.name
- creditorIban = "DE64500105178797276788"
- creditorBic = "DEUTDEBB101"
- creditorName = "Max Mustermann"
- subject = "sample transaction
$transactionReferenceDbit"
- this.amount = amount.toString()
- currency = account.currency
- date = getUTCnow().toInstant().toEpochMilli()
- accountServicerReference = transactionReferenceDbit
- this.account = account
- direction = "DBIT"
- }
+ run {
+ val amount = kotlin.random.Random.nextLong(5, 25)
+
+ tech.libeufin.sandbox.BankAccountTransactionEntity.new {
+ debtorIban = account.iban
+ debtorBic = account.bic
+ debtorName = account.name
+ creditorIban = "DE64500105178797276788"
+ creditorBic = "DEUTDEBB101"
+ creditorName = "Max Mustermann"
+ subject = "sample transaction
$transactionReferenceDbit"
+ this.amount = amount.toString()
+ currency = account.currency
+ date = getUTCnow().toInstant().toEpochMilli()
+ accountServicerReference = transactionReferenceDbit
+ this.account = account
+ direction = "DBIT"
}
}
- call.respond(object {})
}
- /**
- * Creates a new Ebics subscriber.
- */
- post("/admin/ebics/subscribers") {
- requireSuperuser(call.request)
- val body = call.receiveJson<EbicsSubscriberElement>()
- transaction {
- EbicsSubscriberEntity.new {
- partnerId = body.partnerID
- userId = body.userID
- systemId = null
- hostId = body.hostID
- state = SubscriberState.NEW
- nextOrderID = 1
- }
+ call.respond(object {})
+ }
+ /**
+ * Creates a new Ebics subscriber.
+ */
+ post("/admin/ebics/subscribers") {
+ requireSuperuser(call.request)
+ val body = call.receiveJson<EbicsSubscriberElement>()
+ transaction {
+ tech.libeufin.sandbox.EbicsSubscriberEntity.new {
+ partnerId = body.partnerID
+ userId = body.userID
+ systemId = null
+ hostId = body.hostID
+ state = tech.libeufin.sandbox.SubscriberState.NEW
+ nextOrderID = 1
}
- call.respondText(
- "Subscriber created.",
- ContentType.Text.Plain, HttpStatusCode.OK
- )
- return@post
}
- /**
- * Shows all the Ebics subscribers' details.
- */
- get("/admin/ebics/subscribers") {
- requireSuperuser(call.request)
- val ret = AdminGetSubscribers()
- transaction {
- EbicsSubscriberEntity.all().forEach {
- ret.subscribers.add(
- EbicsSubscriberElement(
- userID = it.userId,
- partnerID = it.partnerId,
- hostID = it.hostId
- )
+ call.respondText(
+ "Subscriber created.",
+ io.ktor.http.ContentType.Text.Plain,
io.ktor.http.HttpStatusCode.OK
+ )
+ return@post
+ }
+ /**
+ * Shows all the Ebics subscribers' details.
+ */
+ get("/admin/ebics/subscribers") {
+ requireSuperuser(call.request)
+ val ret = AdminGetSubscribers()
+ transaction {
+ tech.libeufin.sandbox.EbicsSubscriberEntity.all().forEach {
+ ret.subscribers.add(
+ EbicsSubscriberElement(
+ userID = it.userId,
+ partnerID = it.partnerId,
+ hostID = it.hostId
)
- }
- }
- call.respond(ret)
- return@get
- }
- post("/admin/ebics/hosts/{hostID}/rotate-keys") {
- requireSuperuser(call.request)
- val hostID: String = call.parameters["hostID"] ?: throw
SandboxError(
- HttpStatusCode.BadRequest, "host ID missing in URL"
- )
- transaction {
- val host = EbicsHostEntity.find {
- EbicsHostsTable.hostID eq hostID
- }.firstOrNull() ?: throw SandboxError(
- HttpStatusCode.NotFound, "Host $hostID not found"
)
- val pairA = CryptoUtil.generateRsaKeyPair(2048)
- val pairB = CryptoUtil.generateRsaKeyPair(2048)
- val pairC = CryptoUtil.generateRsaKeyPair(2048)
- host.authenticationPrivateKey =
ExposedBlob(pairA.private.encoded)
- host.encryptionPrivateKey =
ExposedBlob(pairB.private.encoded)
- host.signaturePrivateKey =
ExposedBlob(pairC.private.encoded)
}
- call.respondText(
- "Keys of '${hostID}' rotated.",
- ContentType.Text.Plain,
- HttpStatusCode.OK
+ }
+ call.respond(ret)
+ return@get
+ }
+ post("/admin/ebics/hosts/{hostID}/rotate-keys") {
+ requireSuperuser(call.request)
+ val hostID: String = call.parameters["hostID"] ?: throw
SandboxError(
+ io.ktor.http.HttpStatusCode.BadRequest, "host ID missing in
URL"
+ )
+ transaction {
+ val host = tech.libeufin.sandbox.EbicsHostEntity.find {
+ tech.libeufin.sandbox.EbicsHostsTable.hostID eq hostID
+ }.firstOrNull() ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound, "Host $hostID not
found"
)
- return@post
+ val pairA =
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ val pairB =
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ val pairC =
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ host.authenticationPrivateKey =
ExposedBlob(pairA.private.encoded)
+ host.encryptionPrivateKey = ExposedBlob(pairB.private.encoded)
+ host.signaturePrivateKey = ExposedBlob(pairC.private.encoded)
}
+ call.respondText(
+ "Keys of '${hostID}' rotated.",
+ io.ktor.http.ContentType.Text.Plain,
+ io.ktor.http.HttpStatusCode.OK
+ )
+ return@post
+ }
- /**
- * Creates a new EBICS host.
- */
- post("/admin/ebics/hosts") {
- requireSuperuser(call.request)
- val req = call.receiveJson<EbicsHostCreateRequest>()
- val pairA = CryptoUtil.generateRsaKeyPair(2048)
- val pairB = CryptoUtil.generateRsaKeyPair(2048)
- val pairC = CryptoUtil.generateRsaKeyPair(2048)
- transaction {
- EbicsHostEntity.new {
- this.ebicsVersion = req.ebicsVersion
- this.hostId = req.hostID
- this.authenticationPrivateKey =
ExposedBlob(pairA.private.encoded)
- this.encryptionPrivateKey =
ExposedBlob(pairB.private.encoded)
- this.signaturePrivateKey =
ExposedBlob(pairC.private.encoded)
- }
+ /**
+ * Creates a new EBICS host.
+ */
+ post("/admin/ebics/hosts") {
+ requireSuperuser(call.request)
+ val req = call.receiveJson<EbicsHostCreateRequest>()
+ val pairA = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ val pairB = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ val pairC = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+ transaction {
+ tech.libeufin.sandbox.EbicsHostEntity.new {
+ this.ebicsVersion = req.ebicsVersion
+ this.hostId = req.hostID
+ this.authenticationPrivateKey =
ExposedBlob(pairA.private.encoded)
+ this.encryptionPrivateKey =
ExposedBlob(pairB.private.encoded)
+ this.signaturePrivateKey =
ExposedBlob(pairC.private.encoded)
}
- call.respondText(
- "Host '${req.hostID}' created.",
- ContentType.Text.Plain,
- HttpStatusCode.OK
- )
- return@post
}
+ call.respondText(
+ "Host '${req.hostID}' created.",
+ io.ktor.http.ContentType.Text.Plain,
+ io.ktor.http.HttpStatusCode.OK
+ )
+ return@post
+ }
+ /**
+ * Show the names of all the Ebics hosts
+ */
+ get("/admin/ebics/hosts") {
+ requireSuperuser(call.request)
+ val ebicsHosts = transaction {
+ tech.libeufin.sandbox.EbicsHostEntity.all().map { it.hostId }
+ }
+ call.respond(EbicsHostsResponse(ebicsHosts))
+ }
+ /**
+ * Serves all the Ebics requests.
+ */
+ post("/ebicsweb") {
+ try {
+ call.ebicsweb()
+ }
/**
- * Show the names of all the Ebics hosts
+ * Those errors were all detected by the bank's logic.
*/
- get("/admin/ebics/hosts") {
- requireSuperuser(call.request)
- val ebicsHosts = transaction {
- EbicsHostEntity.all().map { it.hostId }
+ catch (e: SandboxError) {
+ // Should translate to EBICS error code.
+ when (e.errorCode) {
+
tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw
EbicsProcessingError("Invalid bank state.")
+
tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> throw
EbicsProcessingError("Inconsistent bank state.")
+ else -> throw EbicsProcessingError("Unknown LibEuFin error
code: ${e.errorCode}.")
}
- call.respond(EbicsHostsResponse(ebicsHosts))
+
}
/**
- * Serves all the Ebics requests.
+ * An error occurred, but it wasn't explicitly thrown by the bank.
*/
- post("/ebicsweb") {
- try {
- call.ebicsweb()
- }
- /**
- * Those errors were all detected by the bank's logic.
- */
- catch (e: SandboxError) {
- // Should translate to EBICS error code.
- when (e.errorCode) {
- LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw
EbicsProcessingError("Invalid bank state.")
- LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE ->
throw EbicsProcessingError("Inconsistent bank state.")
- else -> throw EbicsProcessingError("Unknown LibEuFin
error code: ${e.errorCode}.")
- }
+ catch (e: Exception) {
+ throw EbicsProcessingError("Unmanaged error: $e")
+ }
- }
- /**
- * An error occurred, but it wasn't explicitly thrown by the
bank.
- */
- catch (e: Exception) {
- throw EbicsProcessingError("Unmanaged error: $e")
- }
+ }
+ /**
+ * Activates a withdraw operation of 1 currency unit with
+ * the default exchange, from a designated/constant customer.
+ */
+ get("/taler") {
+ requireSuperuser(call.request)
+ SandboxAssert(
+ hostName != null,
+ "Own hostname not found. Logs should have warned"
+ )
+ SandboxAssert(
+ currencyEnv != null,
+ "Currency not found. Logs should have warned"
+ )
+ // check that the three canonical accounts exist
+ val wo = transaction {
+ val exchange = tech.libeufin.sandbox.BankAccountEntity.find {
+ tech.libeufin.sandbox.BankAccountsTable.label eq
"sandbox-account-exchange"
+ }.firstOrNull()
+ val customer = tech.libeufin.sandbox.BankAccountEntity.find {
+ tech.libeufin.sandbox.BankAccountsTable.label eq
"sandbox-account-customer"
+ }.firstOrNull()
+ val merchant = tech.libeufin.sandbox.BankAccountEntity.find {
+ tech.libeufin.sandbox.BankAccountsTable.label eq
"sandbox-account-merchant"
+ }.firstOrNull()
+
+ SandboxAssert(exchange != null, "exchange has no bank account")
+ SandboxAssert(customer != null, "customer has no bank account")
+ SandboxAssert(merchant != null, "merchant has no bank account")
+
+ // At this point, the three actors exist and a new withdraw
operation can be created.
+ tech.libeufin.sandbox.TalerWithdrawalEntity.new {
+ // wopid is autogenerated, and momentarily the only column
+ }
}
/**
- * Activates a withdraw operation of 1 currency unit with
- * the default exchange, from a designated/constant customer.
+ * Future versions will include the QR code in this response.
*/
- get("/taler") {
- requireSuperuser(call.request)
- SandboxAssert(
- hostName != null,
- "Own hostname not found. Logs should have warned"
- )
- SandboxAssert(
- currencyEnv != null,
- "Currency not found. Logs should have warned"
+ call.respondText("taler://withdraw/${hostName}/api/${wo.wopid}")
+ return@get
+ }
+ get("/api/config") {
+ SandboxAssert(
+ currencyEnv != null,
+ "Currency not found. Logs should have warned"
+ )
+ call.respond(object {
+ val name = "taler-bank-integration"
+
+ // FIXME: use actual version here!
+ val version = "0:0:0"
+ val currency = currencyEnv
+ })
+ }
+ /**
+ * not regulating the access here, as the wopid was only granted
+ * to logged-in users before (at the /taler endpoint) and has enough
+ * entropy to prevent guesses.
+ */
+ get("/api/withdrawal-operation/{wopid}") {
+ val wopid: String = ensureNonNull(call.parameters["wopid"])
+ val wo = transaction {
+
+ tech.libeufin.sandbox.TalerWithdrawalEntity.find {
+ tech.libeufin.sandbox.TalerWithdrawalsTable.wopid eq
java.util.UUID.fromString(wopid)
+ }.firstOrNull() ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound,
+ "Withdrawal operation: $wopid not found"
)
- // check that the three canonical accounts exist
- val wo = transaction {
- val exchange = BankAccountEntity.find {
- BankAccountsTable.label eq "sandbox-account-exchange"
- }.firstOrNull()
- val customer = BankAccountEntity.find {
- BankAccountsTable.label eq "sandbox-account-customer"
- }.firstOrNull()
- val merchant = BankAccountEntity.find {
- BankAccountsTable.label eq "sandbox-account-merchant"
- }.firstOrNull()
-
- SandboxAssert(exchange != null, "exchange has no bank
account")
- SandboxAssert(customer != null, "customer has no bank
account")
- SandboxAssert(merchant != null, "merchant has no bank
account")
-
- // At this point, the three actors exist and a new
withdraw operation can be created.
- TalerWithdrawalEntity.new {
- // wopid is autogenerated, and momentarily the only
column
-
- }
- }
- /**
- * Future versions will include the QR code in this response.
- */
-
call.respondText("taler://withdraw/${hostName}/api/${wo.wopid}")
- return@get
}
- get("/api/config") {
- SandboxAssert(
- currencyEnv != null,
- "Currency not found. Logs should have warned"
+ SandboxAssert(
+ envName != null,
+ "Env name not found, cannot suggest Exchange."
+ )
+ val ret = TalerWithdrawalStatus(
+ selection_done = wo.selectionDone,
+ transfer_done = wo.transferDone,
+ amount = "${currencyEnv}:5",
+ suggested_exchange = "https://exchange.${envName}.taler.net/"
+ )
+ call.respond(ret)
+ return@get
+ }
+ /**
+ * Here Sandbox collects the reserve public key to be used
+ * as the wire transfer subject, and pays the exchange - which
+ * is as well collected in this request.
+ */
+ post("/api/withdrawal-operation/{wopid}") {
+
+ val wopid: String = ensureNonNull(call.parameters["wopid"])
+ val body = call.receiveJson<TalerWithdrawalConfirmation>()
+
+ newSuspendedTransaction(context = singleThreadContext) {
+ var wo = tech.libeufin.sandbox.TalerWithdrawalEntity.find {
+ tech.libeufin.sandbox.TalerWithdrawalsTable.wopid eq
java.util.UUID.fromString(wopid)
+ }.firstOrNull() ?: throw SandboxError(
+ io.ktor.http.HttpStatusCode.NotFound, "Withdrawal
operation $wopid not found."
)
- call.respond(object {
- val name = "taler-bank-integration"
-
- // FIXME: use actual version here!
- val version = "0:0:0"
- val currency = currencyEnv
- })
- }
- /**
- * not regulating the access here, as the wopid was only granted
- * to logged-in users before (at the /taler endpoint) and has
enough
- * entropy to prevent guesses.
- */
- get("/api/withdrawal-operation/{wopid}") {
- val wopid: String = ensureNonNull(call.parameters["wopid"])
- val wo = transaction {
-
- TalerWithdrawalEntity.find {
- TalerWithdrawalsTable.wopid eq UUID.fromString(wopid)
- }.firstOrNull() ?: throw SandboxError(
- HttpStatusCode.NotFound,
- "Withdrawal operation: $wopid not found"
+ if (wo.selectionDone) {
+ if (wo.transferDone) {
+ logger.info("Wallet performs again this operation that
was paid out earlier: idempotent")
+ return@newSuspendedTransaction
+ }
+ // reservePub+exchange selected but not payed: check
consistency
+ if (body.reserve_pub != wo.reservePub) throw SandboxError(
+ io.ktor.http.HttpStatusCode.Conflict,
+ "Selecting a different reserve from the one already
selected"
+ )
+ if (body.selected_exchange != wo.selectedExchangePayto)
throw SandboxError(
+ io.ktor.http.HttpStatusCode.Conflict,
+ "Selecting a different exchange from the one already
selected"
)
}
- SandboxAssert(
- envName != null,
- "Env name not found, cannot suggest Exchange."
- )
- val ret = TalerWithdrawalStatus(
- selection_done = wo.selectionDone,
- transfer_done = wo.transferDone,
- amount = "${currencyEnv}:5",
- suggested_exchange =
"https://exchange.${envName}.taler.net/"
+ // here only if (1) no selection done or (2) _only_ selection
done:
+ // both ways no transfer must have happened.
+ SandboxAssert(!wo.transferDone, "Sandbox allowed paid but
unselected reserve")
+
+ wireTransfer(
+ "sandbox-account-customer",
+ "sandbox-account-exchange",
+ "$currencyEnv:5",
+ body.reserve_pub
)
- call.respond(ret)
- return@get
+ wo.reservePub = body.reserve_pub
+ wo.selectedExchangePayto = body.selected_exchange
+ wo.selectionDone = true
+ wo.transferDone = true
}
/**
- * Here Sandbox collects the reserve public key to be used
- * as the wire transfer subject, and pays the exchange - which
- * is as well collected in this request.
+ * NOTE: is this always guaranteed to run AFTER the suspended
+ * transaction block above?
*/
- post("/api/withdrawal-operation/{wopid}") {
+ call.respond(object {
+ val transfer_done = true
+ })
+ return@post
+ }
- val wopid: String = ensureNonNull(call.parameters["wopid"])
- val body = call.receiveJson<TalerWithdrawalConfirmation>()
+ // Create a new demobank instance with a particular currency,
+ // debt limit and possibly other configuration
+ // (could also be a CLI command for now)
+ post("/demobanks") {
- newSuspendedTransaction(context = singleThreadContext) {
- var wo = TalerWithdrawalEntity.find {
- TalerWithdrawalsTable.wopid eq UUID.fromString(wopid)
- }.firstOrNull() ?: throw SandboxError(
- HttpStatusCode.NotFound, "Withdrawal operation $wopid
not found."
- )
- if (wo.selectionDone) {
- if (wo.transferDone) {
- logger.info("Wallet performs again this operation
that was paid out earlier: idempotent")
- return@newSuspendedTransaction
- }
- // reservePub+exchange selected but not payed: check
consistency
- if (body.reserve_pub != wo.reservePub) throw
SandboxError(
- HttpStatusCode.Conflict,
- "Selecting a different reserve from the one
already selected"
- )
- if (body.selected_exchange !=
wo.selectedExchangePayto) throw SandboxError(
- HttpStatusCode.Conflict,
- "Selecting a different exchange from the one
already selected"
- )
- }
- // here only if (1) no selection done or (2) _only_
selection done:
- // both ways no transfer must have happened.
- SandboxAssert(!wo.transferDone, "Sandbox allowed paid but
unselected reserve")
-
- wireTransfer(
- "sandbox-account-customer",
- "sandbox-account-exchange",
- "$currencyEnv:5",
- body.reserve_pub
- )
- wo.reservePub = body.reserve_pub
- wo.selectedExchangePayto = body.selected_exchange
- wo.selectionDone = true
- wo.transferDone = true
- }
- /**
- * NOTE: is this always guaranteed to run AFTER the suspended
- * transaction block above?
- */
- call.respond(object {
- val transfer_done = true
- })
- return@post
- }
+ }
- // Create a new demobank instance with a particular currency,
- // debt limit and possibly other configuration
- // (could also be a CLI command for now)
- post("/demobanks") {
+ // List configured demobanks
+ get("/demobanks") {
- }
+ }
- // List configured demobanks
- get("/demobanks") {
+ delete("/demobank/{demobankid") {
- }
+ }
- delete("/demobank/{demobankid") {
+ get("/demobank/{demobankid") {
- }
+ }
- get("/demobank/{demobankid") {
+ route("/demobank/{demobankid}") {
+ // Note: Unlike the old pybank, the sandbox does *not* actually
expose the
+ // taler wire gateway API, because the exchange uses the nexus.
- }
+ // Endpoint(s) for making arbitrary payments in the sandbox for
integration tests
+ // FIXME: Do we actually need this, or can we just use the sandbox
admin APIs?
+ route("/testing-api") {
- route("/demobank/{demobankid}") {
- // Note: Unlike the old pybank, the sandbox does *not*
actually expose the
- // taler wire gateway API, because the exchange uses the nexus.
+ }
- // Endpoint(s) for making arbitrary payments in the sandbox
for integration tests
- // FIXME: Do we actually need this, or can we just use the
sandbox admin APIs?
- route("/testing-api") {
+ route("/access-api") {
+ get("/accounts/{account_name}") {
+ // Authenticated. Accesses basic information (balance)
+ // about an account. (see docs)
+ // FIXME: Since we now use IBANs everywhere, maybe the
account should also be assigned an IBAN
}
- route("/access-api") {
- get("/accounts/{account_name}") {
- // Authenticated. Accesses basic information (balance)
- // about an account. (see docs)
-
- // FIXME: Since we now use IBANs everywhere, maybe the
account should also be assigned an IBAN
- }
-
- get("/accounts/{account_name}/history") {
- // New endpoint, access account history to display in
the SPA
- // (could be merged with GET /accounts/{account_name}
- }
+ get("/accounts/{account_name}/history") {
+ // New endpoint, access account history to display in the
SPA
+ // (could be merged with GET /accounts/{account_name}
+ }
- // [...]
+ // [...]
- get("/public-accounts") {
- // List public accounts. Does not require any
authentication.
- // XXX: New!
- }
-
- get("/public-accounts/{account_name}/history") {
- // Get transaction history of a public account
- }
+ get("/public-accounts") {
+ // List public accounts. Does not require any
authentication.
+ // XXX: New!
+ }
- post("/testing/register") {
- // Register a new account.
- // No authentication is required to register a new
user.
- // FIXME: Should probably not use "testing" as the
prefix, since it's used "in production" in the demobank SPA
- }
+ get("/public-accounts/{account_name}/history") {
+ // Get transaction history of a public account
}
+ post("/testing/register") {
+ // Register a new account.
+ // No authentication is required to register a new user.
+ // FIXME: Should probably not use "testing" as the
prefix, since it's used "in production" in the demobank SPA
+ }
}
+
}
}
+}
+fun serverMain(dbName: String, port: Int) {
+ execThrowableOrTerminate { dbCreateTables(dbName) }
+ val server = embeddedServer(Netty, port = port, module = sandboxApp)
logger.info("LibEuFin Sandbox running on port $port")
try {
server.start(wait = true)
diff --git a/sandbox/src/test/kotlin/CamtTest.kt
b/sandbox/src/test/kotlin/CamtTest.kt
deleted file mode 100644
index 56a53a4..0000000
--- a/sandbox/src/test/kotlin/CamtTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.sandbox.buildCamtString
-import tech.libeufin.util.RawPayment
-import tech.libeufin.util.XMLUtil
-import kotlin.test.assertTrue
-
-class CamtTest {
-
- @Test
- fun validationTest() {
- val payment = RawPayment(
- creditorIban = "GB33BUKB20201222222222",
- creditorName = "Oliver Smith",
- creditorBic = "BUKBGB33",
- debtorIban = "GB33BUKB20201333333333",
- debtorName = "John Doe",
- debtorBic = "BUKBGB33",
- amount = "2",
- currency = "EUR",
- subject = "reimbursement",
- date = "1000-02-02",
- uid = "0",
- direction = "DBIT"
- )
- val xml = buildCamtString(
- 53,
- "GB33BUKB20201222222222",
- mutableListOf(payment)
- )
- assertTrue {
- XMLUtil.validateFromString(xml)
- }
- }
-}
\ No newline at end of file
diff --git a/sandbox/src/test/kotlin/DBTest.kt
b/sandbox/src/test/kotlin/DBTest.kt
index e52373f..5c99acf 100644
--- a/sandbox/src/test/kotlin/DBTest.kt
+++ b/sandbox/src/test/kotlin/DBTest.kt
@@ -94,7 +94,7 @@ class DBTest {
addLogger(StdOutSqlLogger)
BankAccountTransactionEntity.find {
BankAccountTransactionsTable.date.between(
- parseDashedDate("1970-01-01").millis(),
+
parseDashedDate("1970-01-01").toInstant().toEpochMilli(),
LocalDateTime.now().millis()
)
}.firstOrNull()
diff --git a/util/build.gradle b/util/build.gradle
index 87843cb..6f67b78 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -27,6 +27,8 @@ sourceSets {
}
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'
@@ -48,6 +50,10 @@ dependencies {
implementation "org.jetbrains.exposed:exposed-core:$exposed_version"
implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
+ 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"
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
diff --git a/util/src/main/kotlin/UnixDomainSocket.kt
b/util/src/main/kotlin/UnixDomainSocket.kt
new file mode 100644
index 0000000..b2713f2
--- /dev/null
+++ b/util/src/main/kotlin/UnixDomainSocket.kt
@@ -0,0 +1,97 @@
+import io.ktor.application.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.http.HttpMethod
+import io.ktor.server.engine.*
+import io.ktor.server.testing.*
+import io.netty.bootstrap.ServerBootstrap
+import io.netty.buffer.ByteBufInputStream
+import io.netty.buffer.Unpooled
+import io.netty.channel.*
+import io.netty.channel.epoll.EpollEventLoopGroup
+import io.netty.channel.epoll.EpollServerDomainSocketChannel
+import io.netty.channel.unix.DomainSocketAddress
+import io.netty.handler.codec.http.*
+import io.netty.handler.codec.http.DefaultHttpResponse
+import io.netty.util.AttributeKey
+
+fun startServer(unixSocketPath: String, app: Application.() -> Unit) {
+
+ val boss = EpollEventLoopGroup()
+ val worker = EpollEventLoopGroup()
+ val serverBootstrap = ServerBootstrap()
+ serverBootstrap.group(boss, worker).channel(
+ EpollServerDomainSocketChannel::class.java
+ ).childHandler(LibeufinHttpInit(app))
+
+ val socketPath = DomainSocketAddress(unixSocketPath)
+ serverBootstrap.bind(socketPath).sync().channel().closeFuture().sync()
+}
+
+private val ktorApplicationKey = AttributeKey.newInstance<Application.() ->
Unit>("KtorApplicationCall")
+
+class LibeufinHttpInit(private val app: Application.() -> Unit) :
ChannelInitializer<Channel>() {
+ override fun initChannel(ch: Channel) {
+ val libeufinHandler = LibeufinHttpHandler()
+ ch.pipeline(
+ ).addLast(
+ HttpServerCodec()
+ ).addLast(
+ HttpObjectAggregator(Int.MAX_VALUE)
+ ).addLast(
+ libeufinHandler
+ )
+ val libeufinCtx: ChannelHandlerContext =
ch.pipeline().context(libeufinHandler)
+ libeufinCtx.attr(ktorApplicationKey).set(app)
+ }
+}
+
+class LibeufinHttpHandler : SimpleChannelInboundHandler<FullHttpRequest>() {
+
+ @OptIn(EngineAPI::class)
+ override fun channelRead0(ctx: ChannelHandlerContext?, msg:
FullHttpRequest) {
+ val app = ctx?.attr(ktorApplicationKey)?.get()
+ if (app == null) throw UtilError(
+ HttpStatusCode.InternalServerError,
+ "custom libEufin Unix-domain-socket+HTTP handler lost its Web app",
+ null
+ )
+ /**
+ * Below is only a echo of what euFin gets from the network. All
+ * the checks should then occur at the Web app + Ktor level. Hence,
+ * a HTTP call of GET with a non-empty body is not to be blocked /
warned
+ * at this level.
+ *
+ * The only exception is the HTTP version value in the response, as the
+ * response returned by the Web app does not set it. Therefore, this
+ * proxy echoes back the HTTP version that was read in the request.
+ */
+ withTestApplication(app) {
+ val httpVersion = msg.protocolVersion()
+ // Proxying the request with Ktor API.
+ val call = handleRequest(closeRequest = false) {
+ msg.headers().forEach { addHeader(it.key, it.value) }
+ 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 with Netty API.
+ val response = DefaultFullHttpResponse(
+ httpVersion,
+ HttpResponseStatus.valueOf(statusCode),
+ Unpooled.wrappedBuffer(call.response.byteContent ?:
ByteArray(0))
+ )
+ call.response.headers.allValues().forEach { s, list ->
+ response.headers().set(s, list.joinToString()) //
joinToString() separates with ", " by default.
+ }
+ ctx.write(response)
+ ctx.flush()
+ }
+ }
+}
\ No newline at end of file
diff --git a/util/src/test/kotlin/DomainSocketTest.kt
b/util/src/test/kotlin/DomainSocketTest.kt
new file mode 100644
index 0000000..d6e5c18
--- /dev/null
+++ b/util/src/test/kotlin/DomainSocketTest.kt
@@ -0,0 +1,30 @@
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.response.*
+import io.ktor.routing.*
+import org.junit.Test
+import io.ktor.jackson.jackson
+import io.ktor.request.*
+
+class DomainSocketTest {
+ // @Test
+ fun bind() {
+ startServer("/tmp/java.sock") {
+ install(ContentNegotiation) { jackson() }
+ routing {
+ // responds with a empty JSON object.
+ get("/") {
+ this.call.respond(object {})
+ return@get
+ }
+ // echoes what it read in the request.
+ post("/") {
+ val body = this.call.receiveText()
+ this.call.respondText(body)
+ return@post
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [libeufin] branch master updated (fe31400 -> 6144c4a),
gnunet <=
- [libeufin] 01/07: Unix domain socket server., gnunet, 2021/10/06
- [libeufin] 02/07: Unix domain sockets dependencies, gnunet, 2021/10/06
- [libeufin] 03/07: adapt test to new API, gnunet, 2021/10/06
- [libeufin] 06/07: Unix domain socket proxy: fix payload read/write., gnunet, 2021/10/06
- [libeufin] 04/07: test for Unix domain socket, gnunet, 2021/10/06
- [libeufin] 05/07: test dep, gnunet, 2021/10/06
- [libeufin] 07/07: Serve Sandbox via Unix domain socket., gnunet, 2021/10/06