libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit b8a29f97b6b4c571f53b81b2aa2acb8d46415a17
parent 9ad7d47e0bcff24c71429be2ac3e7a2a68c99247
Author: Antoine A <>
Date:   Mon, 12 Feb 2024 16:18:36 +0100

Fix BodyLimitAndDecompression plugin

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Error.kt | 5+++++
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 55++++++++++++++++++++++++++++++++-----------------------
Mbank/src/test/kotlin/SecurityTest.kt | 26++++++++++++++++++++++++--
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 2+-
Mcommon/src/main/kotlin/Client.kt | 19++++---------------
5 files changed, 66 insertions(+), 41 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -120,6 +120,11 @@ fun badRequest( detail: String? = null ): LibeufinException = libeufinError(HttpStatusCode.BadRequest, hint, error, detail) +fun unsupportedMediaType( + hint: String, + error: TalerErrorCode = TalerErrorCode.END, +): LibeufinException = libeufinError(HttpStatusCode.UnsupportedMediaType, hint, error) + /* ----- Currency checks ----- */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -70,32 +70,41 @@ val bodyPlugin = createApplicationPlugin("BodyLimitAndDecompression") { transformBody { body -> val bytes = ByteArray(MAX_BODY_LENGTH.toInt() + 1) var read = 0; - if (call.request.headers[HttpHeaders.ContentEncoding] == "deflate") { - // Decompress and check decompressed length - val inflater = Inflater() - while (!body.isClosedForRead) { - body.read { buf -> - inflater.setInput(buf) - try { - read += inflater.inflate(bytes, read, bytes.size - read) - } catch (e: DataFormatException) { - logger.error("Deflated request failed to inflate: ${e.message}") - throw badRequest( - "Could not inflate request", - TalerErrorCode.GENERIC_COMPRESSION_INVALID - ) + when (val encoding = call.request.headers[HttpHeaders.ContentEncoding]) { + "deflate" -> { + // Decompress and check decompressed length + val inflater = Inflater() + while (!body.isClosedForRead) { + body.read { buf -> + inflater.setInput(buf) + try { + read += inflater.inflate(bytes, read, bytes.size - read) + } catch (e: DataFormatException) { + logger.error("Deflated request failed to inflate: ${e.message}") + throw badRequest( + "Could not inflate request", + TalerErrorCode.GENERIC_COMPRESSION_INVALID + ) + } } + if (read > MAX_BODY_LENGTH) + throw badRequest("Decompressed body is suspiciously big > $MAX_BODY_LENGTH B") } - if (read > MAX_BODY_LENGTH) - throw badRequest("Decompressed body is suspiciously big") - } - } else { - // Check body length - while (!body.isClosedForRead) { - read += body.readAvailable(bytes, read, bytes.size - read) - if (read > MAX_BODY_LENGTH) - throw badRequest("Body is suspiciously big") } + null -> { + // Check body length + while (true) { + val new = body.readAvailable(bytes, read, bytes.size - read) + if (new == -1) break; // Channel is closed + read += new + if (read > MAX_BODY_LENGTH) + throw badRequest("Body is suspiciously big > $MAX_BODY_LENGTH B") + } + } + else -> throw unsupportedMediaType( + "Content encoding '$encoding' not supported, expected plain or deflate", + TalerErrorCode.GENERIC_COMPRESSION_INVALID + ) } ByteReadChannel(bytes, 0, read) } diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt @@ -20,14 +20,30 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* +import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.engine.* import io.ktor.server.testing.* import kotlin.test.* import kotlinx.coroutines.* +import kotlinx.serialization.json.* import org.junit.Test import tech.libeufin.bank.* import tech.libeufin.common.* +import tech.libeufin.common.* +import java.io.ByteArrayOutputStream +import java.util.zip.DeflaterOutputStream + +inline fun <reified B> HttpRequestBuilder.jsonDeflate(b: B) { + val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b); + contentType(ContentType.Application.Json) + headers.set(HttpHeaders.ContentEncoding, "deflate") + val bos = ByteArrayOutputStream() + val ios = DeflaterOutputStream(bos) + ios.write(json.toByteArray()) + ios.finish() + setBody(bos.toByteArray()) +} class SecurityTest { @Test @@ -49,10 +65,16 @@ class SecurityTest { // Check body too big even after compression client.postA("/accounts/merchant/transactions") { - json(valid_req, deflate = true) { + jsonDeflate(obj(valid_req) { "payto_uri" to "$exchangePayto?message=payout${"A".repeat(4100)}" - } + }) }.assertBadRequest() + + // Check uknown encoding + client.postA("/accounts/merchant/transactions") { + headers.set(HttpHeaders.ContentEncoding, "unknown") + json(valid_req) + }.assertStatus(HttpStatusCode.UnsupportedMediaType, TalerErrorCode.GENERIC_COMPRESSION_INVALID) } } diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -220,7 +220,7 @@ class WireGatewayApiTest { // Giving debt allowance and checking the OK case. setMaxDebt("merchant", "KUDOS:1000") client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { - json(valid_req, deflate = true) + json(valid_req) }.assertOk() // Trigger conflict due to reused reserve_pub diff --git a/common/src/main/kotlin/Client.kt b/common/src/main/kotlin/Client.kt @@ -24,7 +24,6 @@ import kotlinx.serialization.json.* import io.ktor.client.request.* import io.ktor.client.statement.* import java.io.ByteArrayOutputStream -import java.util.zip.DeflaterOutputStream import kotlin.test.assertEquals /* ----- Json DSL ----- */ @@ -46,27 +45,17 @@ class JsonBuilder(from: JsonObject) { /* ----- Json body helper ----- */ -inline fun <reified B> HttpRequestBuilder.json(b: B, deflate: Boolean = false) { +inline fun <reified B> HttpRequestBuilder.json(b: B) { val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b); contentType(ContentType.Application.Json) - if (deflate) { - headers.set("Content-Encoding", "deflate") - val bos = ByteArrayOutputStream() - val ios = DeflaterOutputStream(bos) - ios.write(json.toByteArray()) - ios.finish() - setBody(bos.toByteArray()) - } else { - setBody(json) - } + setBody(json) } inline fun HttpRequestBuilder.json( - from: JsonObject = JsonObject(emptyMap()), - deflate: Boolean = false, + from: JsonObject = JsonObject(emptyMap()), builderAction: JsonBuilder.() -> Unit ) { - json(obj(from, builderAction), deflate) + json(obj(from, builderAction)) } inline suspend fun <reified B> HttpResponse.json(): B =