commit 7aaa18caacf0d10fa287d75e03e4fb322e128437
parent c62a6ea1b16657bf410c10baf52d7aeb5e790134
Author: Antoine A <>
Date: Mon, 1 Jul 2024 16:02:10 +0200
common: optimize body limit and clean code
Diffstat:
13 files changed, 73 insertions(+), 36 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/Tan.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/Tan.kt
@@ -23,6 +23,7 @@ import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import kotlinx.serialization.json.Json
+import tech.libeufin.common.X_CHALLENGE_ID
import tech.libeufin.bank.*
import tech.libeufin.bank.db.Database
import tech.libeufin.bank.db.TanDAO.Challenge
@@ -71,7 +72,7 @@ suspend inline fun <reified B> ApplicationCall.receiveChallenge(
db: Database,
op: Operation
): Pair<B, Challenge?> {
- val id = request.headers["X-Challenge-Id"]?.toLongOrNull()
+ val id = request.headers[X_CHALLENGE_ID]?.toLongOrNull()
return if (id != null) {
val challenge = db.tan.challenge(id, username, op)!!
Pair(Json.decodeFromString(challenge.body), challenge)
@@ -87,7 +88,7 @@ suspend fun ApplicationCall.checkChallenge(
db: Database,
op: Operation
): Challenge? {
- val id = request.headers["X-Challenge-Id"]?.toLongOrNull()
+ val id = request.headers[X_CHALLENGE_ID]?.toLongOrNull()
return if (id != null) {
db.tan.challenge(id, username, op)!!
} else {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
@@ -109,7 +109,7 @@ fun Route.auth(db: Database, scope: TokenScope, allowAdmin: Boolean = false, req
* Returns the authenticated customer login.
*/
private suspend fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope: TokenScope): String {
- val header = request.headers["Authorization"]
+ val header = request.headers[HttpHeaders.Authorization]
// Basic auth challenge
if (header == null) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -76,7 +76,7 @@ fun ApplicationRequest.talerWithdrawUri(id: UUID) = url {
)
host = "withdraw"
appendPathSegments("${origin.serverHost}:${origin.serverPort}")
- headers["X-Forward-Prefix"]?.let {
+ headers[X_FORWARD_PREFIX]?.let {
appendPathSegments(it)
}
appendPathSegments("taler-integration", id.toString())
@@ -88,7 +88,7 @@ fun ApplicationRequest.withdrawConfirmUrl(id: UUID) = url {
defaultPort = -1
)
host = "${origin.serverHost}:${origin.serverPort}"
- headers["X-Forward-Prefix"]?.let {
+ headers[X_FORWARD_PREFIX]?.let {
appendPathSegments(it)
}
appendEncodedPathSegments("webui", "#", "operation", id.toString())
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -98,7 +98,7 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse> {
val token = it.access_token
client.post("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
json { "scope" to toScope }
}.assertOk()
}
@@ -118,7 +118,7 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse> {
val token = it.access_token
client.post("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
json { "scope" to toScope }
}.assertForbidden(TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT)
}
@@ -132,7 +132,7 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse> {
val token = it.access_token
client.post("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
json { "scope" to "readonly" }
}.assertUnauthorized()
}
@@ -186,11 +186,11 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse>().access_token
// Check OK
client.delete("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
}.assertNoContent()
// Check token no longer work
client.delete("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
}.assertUnauthorized()
// Checking merchant can still be served by basic auth, after token deletion.
@@ -590,7 +590,7 @@ class CoreBankAccountsApiTest {
// Check account can no longer login
client.delete("/accounts/customer/token") {
- headers["Authorization"] = "Bearer $token"
+ headers[HttpHeaders.Authorization] = "Bearer $token"
}.assertUnauthorized()
client.getA("/accounts/customer/transactions/$tx_id").assertUnauthorized()
client.getA("/accounts/customer/cashouts/$cashout_id").assertUnauthorized()
diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -19,6 +19,7 @@
import io.ktor.client.request.*
import io.ktor.http.*
+import io.ktor.http.content.OutputStreamContent
import kotlinx.serialization.json.Json
import org.junit.Test
import tech.libeufin.common.*
@@ -30,6 +31,21 @@ inline fun <reified B> HttpRequestBuilder.jsonDeflate(b: B) {
setBody(json.toByteArray().inputStream().deflate().readBytes())
}
+inline fun <reified B> HttpRequestBuilder.jsonStreamDeflate(b: B) {
+ val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b)
+ headers[HttpHeaders.ContentEncoding] = "deflate"
+ setBody(OutputStreamContent({
+ write(json.toByteArray().inputStream().deflate().readBytes())
+ }, ContentType.Application.Json))
+}
+
+inline fun <reified B> HttpRequestBuilder.jsonStream(b: B) {
+ val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b)
+ setBody(OutputStreamContent({
+ write(json.toByteArray())
+ }, ContentType.Application.Json))
+}
+
class SecurityTest {
@Test
fun bodySizeLimit() = bankSetup {
@@ -37,22 +53,32 @@ class SecurityTest {
"payto_uri" to "$exchangePayto?message=payout"
"amount" to "KUDOS:0.3"
}
+ val too_big = obj(valid_req) {
+ "payto_uri" to "$exchangePayto?message=payout${"A".repeat(MAX_BODY_LENGTH+1)}"
+ }
+
client.postA("/accounts/merchant/transactions") {
json(valid_req)
}.assertOk()
-
+
// Check body too big
client.postA("/accounts/merchant/transactions") {
- json(valid_req) {
- "payto_uri" to "$exchangePayto?message=payout${"A".repeat(4100)}"
- }
+ json(too_big)
}.assertBadRequest()
// Check body too big even after compression
client.postA("/accounts/merchant/transactions") {
- jsonDeflate(obj(valid_req) {
- "payto_uri" to "$exchangePayto?message=payout${"A".repeat(4100)}"
- })
+ jsonDeflate(too_big)
+ }.assertBadRequest()
+
+ // Check streaming body too big
+ client.postA("/accounts/merchant/transactions") {
+ jsonStream(too_big)
+ }.assertBadRequest()
+
+ // Check streaming body too big even after compression
+ client.postA("/accounts/merchant/transactions") {
+ jsonStreamDeflate(too_big)
}.assertBadRequest()
// Check unknown encoding
diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt
@@ -241,7 +241,7 @@ class Bench {
}
measureAction("token_refresh") {
client.post("/accounts/customer/token") {
- headers["Authorization"] = "Bearer ${tokens[it]}"
+ headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
json { "scope" to "readonly" }
}.assertOk()
}
@@ -250,7 +250,7 @@ class Bench {
}
measureAction("token_delete") {
client.delete("/accounts/customer/token") {
- headers["Authorization"] = "Bearer ${tokens[it]}"
+ headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
}.assertNoContent()
}
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
@@ -342,7 +342,7 @@ suspend fun HttpResponse.assertChallenge(
return call.client.request(this.call.request.url) {
pwAuth(username)
method = call.request.method
- headers["X-Challenge-Id"] = "$id"
+ headers[X_CHALLENGE_ID] = "$id"
}
}
diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -50,7 +50,7 @@ suspend fun ApplicationTestBuilder.authRoutine(
// Bad header
client.request(path) {
this.method = method
- headers["Authorization"] = "WTF"
+ headers[HttpHeaders.Authorization] = "WTF"
}.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED)
// Unknown account
diff --git a/common/src/main/kotlin/Constants.kt b/common/src/main/kotlin/Constants.kt
@@ -23,8 +23,12 @@ const val MIN_VERSION: Int = 14
const val SERIALIZATION_RETRY: Int = 10
// Security
-const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB
+const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB
// API version
const val WIRE_GATEWAY_API_VERSION: String = "0:2:0"
-const val REVENUE_API_VERSION: String = "0:0:0"
-\ No newline at end of file
+const val REVENUE_API_VERSION: String = "0:0:0"
+
+// HTTP headers
+const val X_CHALLENGE_ID: String = "X-Challenge-Id"
+const val X_FORWARD_PREFIX: String = "X-Forward-Prefix"
+\ No newline at end of file
diff --git a/common/src/main/kotlin/api/server.kt b/common/src/main/kotlin/api/server.kt
@@ -50,9 +50,14 @@ import java.util.zip.Inflater
fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> {
return createApplicationPlugin("BodyLimitAndDecompression") {
onCallReceive { call ->
- // TODO check content length as an optimisation
+ // Check content length if present and wellformed
+ val contentLenght = call.request.headers[HttpHeaders.ContentLength]?.toIntOrNull()
+ if (contentLenght != null && contentLenght > MAX_BODY_LENGTH)
+ throw badRequest("Body is suspiciously big > ${MAX_BODY_LENGTH}B")
+
+ // Else check while reading and decompressing the body
transformBody { body ->
- val bytes = ByteArray(MAX_BODY_LENGTH.toInt() + 1)
+ val bytes = ByteArray(MAX_BODY_LENGTH + 1)
var read = 0
when (val encoding = call.request.headers[HttpHeaders.ContentEncoding]) {
"deflate" -> {
@@ -72,7 +77,7 @@ fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> {
}
}
if (read > MAX_BODY_LENGTH)
- throw badRequest("Decompressed body is suspiciously big > $MAX_BODY_LENGTH B")
+ throw badRequest("Decompressed body is suspiciously big > ${MAX_BODY_LENGTH}B")
}
}
null -> {
@@ -82,7 +87,7 @@ fun bodyLimitPlugin(logger: Logger): ApplicationPlugin<Unit> {
if (new == -1) break // Channel is closed
read += new
if (read > MAX_BODY_LENGTH)
- throw badRequest("Body is suspiciously big > $MAX_BODY_LENGTH B")
+ throw badRequest("Body is suspiciously big > ${MAX_BODY_LENGTH}B")
}
}
else -> throw unsupportedMediaType(
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt
@@ -33,7 +33,7 @@ fun Route.authApi(cfg: ApiConfig?, callback: Route.() -> Unit): Route =
if (cfg == null) {
throw apiError(HttpStatusCode.NotImplemented, "API not implemented", TalerErrorCode.END)
}
- val header = context.request.headers["Authorization"]
+ val header = context.request.headers[HttpHeaders.Authorization]
// Basic auth challenge
when (cfg.authMethod) {
AuthMethod.None -> {}
diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt
@@ -17,6 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
+import io.ktor.http.*
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
@@ -203,5 +204,5 @@ suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.(
}
fun HttpRequestBuilder.auth() {
- headers["Authorization"] = "Bearer secret-token"
+ headers[HttpHeaders.Authorization] = "Bearer secret-token"
}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/routines.kt b/nexus/src/test/kotlin/routines.kt
@@ -39,13 +39,13 @@ suspend fun ApplicationTestBuilder.authRoutine(
// Bad header
client.request(path) {
this.method = method
- headers["Authorization"] = "WTF"
+ headers[HttpHeaders.Authorization] = "WTF"
}.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED)
// Bad token
client.request(path) {
this.method = method
- headers["Authorization"] = "Bearer bad-token"
+ headers[HttpHeaders.Authorization] = "Bearer bad-token"
}.assertUnauthorized()
// GLS deployment