diff options
author | Antoine A <> | 2024-02-12 16:18:36 +0100 |
---|---|---|
committer | Antoine A <> | 2024-02-12 16:22:54 +0100 |
commit | b8a29f97b6b4c571f53b81b2aa2acb8d46415a17 (patch) | |
tree | c7571a7aaebbf9335b1da5d9ba6991daf95a89ea | |
parent | 9ad7d47e0bcff24c71429be2ac3e7a2a68c99247 (diff) | |
download | libeufin-b8a29f97b6b4c571f53b81b2aa2acb8d46415a17.tar.gz libeufin-b8a29f97b6b4c571f53b81b2aa2acb8d46415a17.tar.bz2 libeufin-b8a29f97b6b4c571f53b81b2aa2acb8d46415a17.zip |
Fix BodyLimitAndDecompression plugin
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Error.kt | 5 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 55 | ||||
-rw-r--r-- | bank/src/test/kotlin/SecurityTest.kt | 26 | ||||
-rw-r--r-- | bank/src/test/kotlin/WireGatewayApiTest.kt | 2 | ||||
-rw-r--r-- | common/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 index eacf6a08..7fd4c3b5 100644 --- 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 index f91d2cdd..f0d8ac89 100644 --- 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 index b3c5f5ac..b5559983 100644 --- 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 index ec20ec15..ba1f3360 100644 --- 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 index 785e3f32..608a11b4 100644 --- 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 = |