From 9a99746f14f4972061e7a5f89c232da2b0674bb9 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 18 Oct 2024 10:52:58 +0200 Subject: [PATCH 1/2] KTOR-7596 Make multipart Content-Type check case-insensitive --- .../jvm/src/io/ktor/http/cio/Multipart.kt | 6 ++---- .../io/ktor/tests/http/cio/MultipartTest.kt | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ktor-http/ktor-http-cio/jvm/src/io/ktor/http/cio/Multipart.kt b/ktor-http/ktor-http-cio/jvm/src/io/ktor/http/cio/Multipart.kt index 8cc049fb369..3eeab2f5afa 100644 --- a/ktor-http/ktor-http-cio/jvm/src/io/ktor/http/cio/Multipart.kt +++ b/ktor-http/ktor-http-cio/jvm/src/io/ktor/http/cio/Multipart.kt @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package io.ktor.http.cio @@ -11,7 +11,6 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.io.* -import kotlinx.io.IOException import kotlinx.io.bytestring.* import java.io.EOFException import java.nio.* @@ -148,14 +147,13 @@ public fun CoroutineScope.parseMultipart( /** * Starts a multipart parser coroutine producing multipart events */ -@Suppress("DEPRECATION_ERROR") public fun CoroutineScope.parseMultipart( input: ByteReadChannel, contentType: CharSequence, contentLength: Long?, maxPartSize: Long = Long.MAX_VALUE, ): ReceiveChannel { - if (!contentType.startsWith("multipart/")) { + if (!contentType.startsWith("multipart/", ignoreCase = true)) { throw IOException("Failed to parse multipart: Content-Type should be multipart/* but it is $contentType") } val boundaryByteBuffer = parseBoundaryInternal(contentType) diff --git a/ktor-http/ktor-http-cio/jvm/test/io/ktor/tests/http/cio/MultipartTest.kt b/ktor-http/ktor-http-cio/jvm/test/io/ktor/tests/http/cio/MultipartTest.kt index 7435028d602..ca9372fbbfc 100644 --- a/ktor-http/ktor-http-cio/jvm/test/io/ktor/tests/http/cio/MultipartTest.kt +++ b/ktor-http/ktor-http-cio/jvm/test/io/ktor/tests/http/cio/MultipartTest.kt @@ -1,6 +1,6 @@ /* -* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. -*/ + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ package io.ktor.tests.http.cio @@ -8,6 +8,8 @@ import io.ktor.http.cio.* import io.ktor.utils.io.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* +import kotlinx.coroutines.test.* +import kotlinx.io.* import kotlin.test.* @OptIn(DelicateCoroutinesApi::class) @@ -335,6 +337,17 @@ class MultipartTest { } } + @Test + fun testParseContentType() = runTest { + fun testContentType(contentType: String) { + parseMultipart(ByteReadChannel.Empty, "$contentType; boundary=A", 0L) + } + + testContentType("multipart/mixed") + testContentType("Multipart/mixed") + assertFailsWith { testContentType("multi-part/mixed") } + } + @OptIn(DelicateCoroutinesApi::class) @Test fun testEmptyPayload() = runBlocking { From 8b32a769fda4c5cda37864f9979d339e1eeaa764 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 18 Oct 2024 12:20:31 +0200 Subject: [PATCH 2/2] Fix case-insensitive checks in other places --- .../plugins/contentnegotiation/JsonContentTypeMatcher.kt | 4 ++-- .../io/ktor/client/plugins/json/JsonContentTypeMatcher.kt | 6 +++--- .../src/io/ktor/server/jetty/jakarta/JettyKtorHandler.kt | 5 ++--- .../jvm/src/io/ktor/server/jetty/JettyKtorHandler.kt | 5 ++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher.kt index fdf137e8b90..0e0ec736efd 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher.kt @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package io.ktor.client.plugins.contentnegotiation @@ -16,6 +16,6 @@ public object JsonContentTypeMatcher : ContentTypeMatcher { } val value = contentType.withoutParameters().toString() - return value.startsWith("application/") && value.endsWith("+json") + return value.startsWith("application/", ignoreCase = true) && value.endsWith("+json", ignoreCase = true) } } diff --git a/ktor-client/ktor-client-plugins/ktor-client-json/common/src/io/ktor/client/plugins/json/JsonContentTypeMatcher.kt b/ktor-client/ktor-client-plugins/ktor-client-json/common/src/io/ktor/client/plugins/json/JsonContentTypeMatcher.kt index 8e4181ed050..5d660ad8693 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-json/common/src/io/ktor/client/plugins/json/JsonContentTypeMatcher.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-json/common/src/io/ktor/client/plugins/json/JsonContentTypeMatcher.kt @@ -1,6 +1,6 @@ /* -* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. -*/ + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ package io.ktor.client.plugins.json @@ -13,6 +13,6 @@ internal class JsonContentTypeMatcher : ContentTypeMatcher { } val value = contentType.withoutParameters().toString() - return value.startsWith("application/") && value.endsWith("+json") + return value.startsWith("application/", ignoreCase = true) && value.endsWith("+json", ignoreCase = true) } } diff --git a/ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyKtorHandler.kt b/ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyKtorHandler.kt index 50b9a6ab496..9d1d522a7fe 100644 --- a/ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyKtorHandler.kt +++ b/ktor-server/ktor-server-jetty-jakarta/jvm/src/io/ktor/server/jetty/jakarta/JettyKtorHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package io.ktor.server.jetty.jakarta @@ -8,7 +8,6 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.response.* -import io.ktor.util.* import io.ktor.util.cio.* import io.ktor.util.pipeline.* import io.ktor.utils.io.* @@ -72,7 +71,7 @@ internal class JettyKtorHandler( ) { try { val contentType = request.contentType - if (contentType != null && contentType.startsWith("multipart/")) { + if (contentType != null && contentType.startsWith("multipart/", ignoreCase = true)) { baseRequest.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multipartConfig) // TODO someone reported auto-cleanup issues so we have to check it } diff --git a/ktor-server/ktor-server-jetty/jvm/src/io/ktor/server/jetty/JettyKtorHandler.kt b/ktor-server/ktor-server-jetty/jvm/src/io/ktor/server/jetty/JettyKtorHandler.kt index f3a8bdbed68..38a12f972a8 100644 --- a/ktor-server/ktor-server-jetty/jvm/src/io/ktor/server/jetty/JettyKtorHandler.kt +++ b/ktor-server/ktor-server-jetty/jvm/src/io/ktor/server/jetty/JettyKtorHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package io.ktor.server.jetty @@ -8,7 +8,6 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.response.* -import io.ktor.util.* import io.ktor.util.cio.* import io.ktor.util.pipeline.* import io.ktor.utils.io.* @@ -72,7 +71,7 @@ internal class JettyKtorHandler( ) { try { val contentType = request.contentType - if (contentType != null && contentType.startsWith("multipart/")) { + if (contentType != null && contentType.startsWith("multipart/", ignoreCase = true)) { baseRequest.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, multipartConfig) // TODO someone reported auto-cleanup issues so we have to check it }