commit 4ddcada57dc252c4ccdb79d573228e25bc2811ed
parent e3a62fa46e75cff879b59725165490943d0d0c1f
Author: Antoine A <>
Date: Wed, 7 Feb 2024 19:02:27 +0100
Generate withdraw URI using request base url
Diffstat:
12 files changed, 36 insertions(+), 159 deletions(-)
diff --git a/bank/build.gradle b/bank/build.gradle
@@ -33,6 +33,7 @@ dependencies {
implementation("io.ktor:ktor-server-status-pages:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
+ implementation("io.ktor:ktor-server-forwarded-header:$ktor_version")
// UNIX domain sockets support (used to connect to PostgreSQL)
implementation("com.kohlschutter.junixsocket:junixsocket-core:$junixsocket_version")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -49,24 +49,19 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
)
call.respond(op.copy(
suggested_exchange = ctx.suggestedWithdrawalExchange,
- confirm_transfer_url = ctx.spaCaptchaURL?.run {
- getWithdrawalConfirmUrl(
- baseUrl = this,
- wopId = uuid
- )
- }
+ confirm_transfer_url = if (op.status == WithdrawalStatus.selected) call.request.withdrawConfirmUrl(uuid) else null
))
}
post("/taler-integration/withdrawal-operation/{wopid}") {
- val opId = call.uuidParameter("wopid")
+ val uuid = call.uuidParameter("wopid")
val req = call.receive<BankWithdrawalOperationPostRequest>()
val res = db.withdrawal.setDetails(
- opId, req.selected_exchange, req.reserve_pub
+ uuid, req.selected_exchange, req.reserve_pub
)
when (res) {
is WithdrawalSelectionResult.UnknownOperation -> throw notFound(
- "Withdrawal operation $opId not found",
+ "Withdrawal operation '$uuid' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
)
is WithdrawalSelectionResult.AlreadySelected -> throw conflict(
@@ -86,25 +81,19 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE
)
is WithdrawalSelectionResult.Success -> {
- val confirmUrl: String? = if (ctx.spaCaptchaURL !== null && res.status == WithdrawalStatus.selected) {
- getWithdrawalConfirmUrl(
- baseUrl = ctx.spaCaptchaURL,
- wopId = opId
- )
- } else null
call.respond(BankWithdrawalOperationPostResponse(
transfer_done = res.status == WithdrawalStatus.confirmed,
status = res.status,
- confirm_transfer_url = confirmUrl
+ confirm_transfer_url = if (res.status == WithdrawalStatus.selected) call.request.withdrawConfirmUrl(uuid) else null
))
}
}
}
post("/taler-integration/withdrawal-operation/{wopid}/abort") {
- val opId = call.uuidParameter("wopid")
- when (db.withdrawal.abort(opId)) {
+ val uuid = call.uuidParameter("wopid")
+ when (db.withdrawal.abort(uuid)) {
AbortResult.UnknownOperation -> throw notFound(
- "Withdrawal operation $opId not found",
+ "Withdrawal operation '$uuid' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
)
AbortResult.AlreadyConfirmed -> throw conflict(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -42,13 +42,6 @@ data class BankConfig(
val defaultDebtLimit: TalerAmount,
val registrationBonus: TalerAmount,
val suggestedWithdrawalExchange: String?,
- /**
- * URL where the user should be redirected to complete the captcha.
- * It can contain the substring "{woid}" that is going to be replaced
- * with the withdrawal operation id and should point where the bank
- * SPA is located.
- */
- val spaCaptchaURL: String?,
val allowConversion: Boolean,
val fiatCurrency: String?,
val fiatCurrencySpec: CurrencySpecification?,
@@ -142,7 +135,6 @@ fun TalerConfig.loadBankConfig(): BankConfig {
defaultDebtLimit = amount("libeufin-bank", "default_debt_limit", regionalCurrency) ?: TalerAmount(0, 0, regionalCurrency),
registrationBonus = amount("libeufin-bank", "registration_bonus", regionalCurrency) ?: TalerAmount(0, 0, regionalCurrency),
suggestedWithdrawalExchange = lookupString("libeufin-bank", "suggested_withdrawal_exchange"),
- spaCaptchaURL = lookupString("libeufin-bank", "spa_captcha_url"),
spaPath = lookupPath("libeufin-bank", "spa"),
allowConversion = allowConversion,
fiatCurrency = fiatCurrency,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -503,12 +503,10 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
TalerErrorCode.BANK_UNALLOWED_DEBIT
)
WithdrawalCreationResult.Success -> {
- val bankBaseUrl = call.request.getBaseUrl()
- ?: throw internalServerError("Bank could not find its own base URL")
call.respond(
BankAccountCreateWithdrawalResponse(
withdrawal_id = opId.toString(),
- taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl, opId.toString())
+ taler_withdraw_uri = call.request.talerWithdrawUri(opId)
)
)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -36,6 +36,7 @@ import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.statuspages.*
+import io.ktor.server.plugins.forwardedheaders.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@@ -120,6 +121,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) {
}
}
}
+ install(XForwardedHeaders)
install(CORS) {
anyHost()
allowHeader(HttpHeaders.Authorization)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -59,32 +59,29 @@ suspend fun ApplicationCall.bankInfo(db: Database, ctx: BankPaytoCtx): BankInfo
*
* https://$BANK_URL/taler-integration
*/
-fun getTalerWithdrawUri(baseUrl: String, woId: String) = url {
- val baseUrlObj = URI(baseUrl).toURL()
+fun ApplicationRequest.talerWithdrawUri(id: UUID) = url {
protocol = URLProtocol(
- name = "taler".plus(if (baseUrlObj.protocol.lowercase() == "http") "+http" else ""), defaultPort = -1
+ name = if (origin.scheme == "http") "taler+http" else "taler",
+ defaultPort = -1
)
host = "withdraw"
- val pathSegments = mutableListOf(
- // adds the hostname(+port) of the actual bank that will serve the withdrawal request.
- baseUrlObj.host.plus(
- if (baseUrlObj.port != -1) ":${baseUrlObj.port}"
- else ""
- )
- )
- // Removing potential double slashes.
- baseUrlObj.path.split("/").forEach {
- if (it.isNotEmpty()) pathSegments.add(it)
+ appendPathSegments(origin.serverHost)
+ headers["X-Forward-Prefix"]?.let {
+ appendPathSegments(it)
}
- pathSegments.add("taler-integration/${woId}")
- this.appendPathSegments(pathSegments)
+ appendPathSegments("taler-integration", id.toString())
}
-// Builds a withdrawal confirm URL.
-fun getWithdrawalConfirmUrl(
- baseUrl: String, wopId: UUID
-): String {
- return baseUrl.replace("{woid}", wopId.toString())
+fun ApplicationRequest.withdrawConfirmUrl(id: UUID) = url {
+ protocol = URLProtocol(
+ name = origin.scheme,
+ defaultPort = -1
+ )
+ host = origin.serverHost
+ headers["X-Forward-Prefix"]?.let {
+ appendPathSegments(it)
+ }
+ appendEncodedPathSegments("webui", "#", "operation", id.toString())
}
fun ApplicationCall.uuidParameter(name: String): UUID {
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -99,12 +99,14 @@ class BankIntegrationApiTest {
json(req)
}.assertOkJson<BankWithdrawalOperationPostResponse> {
assertEquals(WithdrawalStatus.selected, it.status)
+ assertEquals("http://localhost/webui/#/operation/$uuid", it.confirm_transfer_url)
}
// Check idempotence
client.post("/taler-integration/withdrawal-operation/$uuid") {
json(req)
}.assertOkJson<BankWithdrawalOperationPostResponse> {
assertEquals(WithdrawalStatus.selected, it.status)
+ assertEquals("http://localhost/webui/#/operation/$uuid", it.confirm_transfer_url)
}
// Check already selected
client.post("/taler-integration/withdrawal-operation/$uuid") {
@@ -188,34 +190,4 @@ class BankIntegrationApiTest {
client.postA("/taler-integration/withdrawal-operation/${UUID.randomUUID()}/abort")
.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
}
-
- // Testing the generation of taler://withdraw-URIs.
- @Test
- fun testWithdrawUri() {
- // Checking the taler+http://-style.
- val withHttp = getTalerWithdrawUri(
- "http://example.com",
- "my-id"
- )
- assertEquals(withHttp, "taler+http://withdraw/example.com/taler-integration/my-id")
- // Checking the taler://-style
- val onlyTaler = getTalerWithdrawUri(
- "https://example.com/",
- "my-id"
- )
- // Note: this tests as well that no double slashes belong to the result
- assertEquals(onlyTaler, "taler://withdraw/example.com/taler-integration/my-id")
- // Checking the removal of subsequent slashes
- val manySlashes = getTalerWithdrawUri(
- "https://www.example.com//////",
- "my-id"
- )
- assertEquals(manySlashes, "taler://withdraw/www.example.com/taler-integration/my-id")
- // Checking with specified port number
- val withPort = getTalerWithdrawUri(
- "https://www.example.com:9876",
- "my-id"
- )
- assertEquals(withPort, "taler://withdraw/www.example.com:9876/taler-integration/my-id")
- }
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -990,7 +990,9 @@ class CoreBankWithdrawalApiTest {
// Check OK
client.postA("/accounts/merchant/withdrawals") {
json { "amount" to "KUDOS:9.0" }
- }.assertOk()
+ }.assertOkJson<BankAccountCreateWithdrawalResponse> {
+ assertEquals("taler+http://withdraw/localhost/taler-integration/${it.withdrawal_id}", it.taler_withdraw_uri)
+ }
// Check exchange account
client.postA("/accounts/exchange/withdrawals") {
diff --git a/build.gradle b/build.gradle
@@ -22,7 +22,7 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)){
allprojects {
ext {
set("kotlin_version", "1.9.22")
- set("ktor_version", "2.3.7")
+ set("ktor_version", "2.3.8")
set("clikt_version", "4.2.2")
set("coroutines_version", "1.7.3")
set("postgres_version", "42.7.1")
diff --git a/common/src/main/kotlin/HTTP.kt b/common/src/main/kotlin/HTTP.kt
@@ -1,67 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.common
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.server.util.*
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-private val logger: Logger = LoggerFactory.getLogger("libeufin-common")
-
-// Get the base URL of a request, returns null if any problem occurs.
-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.
- var prefix: String = this.headers["X-Forwarded-Prefix"]
- ?: run {
- logger.error("Reverse proxy did not define X-Forwarded-Prefix")
- return null
- }
- if (!prefix.endsWith("/"))
- prefix += "/"
- URLBuilder(
- protocol = URLProtocol(
- name = this.headers["X-Forwarded-Proto"] ?: run {
- logger.error("Reverse proxy did not define X-Forwarded-Proto")
- return null
- },
- defaultPort = -1 // Port must be specified with X-Forwarded-Host.
- ),
- host = this.headers["X-Forwarded-Host"] ?: run {
- logger.error("Reverse proxy did not define X-Forwarded-Host")
- return null
- }
- ).apply {
- encodedPath = prefix
- // Gets dropped otherwise.
- if (!encodedPath.endsWith("/"))
- encodedPath += "/"
- }.buildString()
- } else {
- this.call.url {
- parameters.clear()
- encodedPath = "/"
- }
- }
-}
-\ No newline at end of file
diff --git a/contrib/bank.conf b/contrib/bank.conf
@@ -54,17 +54,9 @@ PORT = 8080
# What should be the file access permissions for UNIXPATH? Only used if SERVE is unix.
# UNIXPATH_MODE = 660
-
# Path to spa files
SPA = $DATADIR/spa/
-# URL that wallets are redirected to when they need to confirm
-# a withdrawal.
-# The string {woid} is replaced with the withdrawal operation ID.
-# FIXME: The name is not great. Maybe call it WITHDRAWAL_CONFIRMATION_REDIRECT
-# or something similar?
-#SPA_CAPTCHA_URL = https://bank.demo.taler.net/webui/#/operation/{woid}
-
# Exchange that is suggested to wallets when withdrawing.
#SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -435,7 +435,7 @@ enum class Document {
}
}
-class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 notifications") {
+class EbicsFetch: CliktCommand("Fetches EBICS files") {
private val common by CommonOption()
private val transient by option(
"--transient",