diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/auth/Authorization.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/auth/Authorization.kt deleted file mode 100644 index 734a6e00..00000000 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/auth/Authorization.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.api.http.adapter.http.feature.auth - -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.util.* -import io.ktor.util.pipeline.* -import net.mamoe.mirai.api.http.context.MahContextHolder -import net.mamoe.mirai.api.http.context.session.Session - -/** - * 拦截 http 请求, 解析 header 并写入可能存在的 sessionKey - */ -object Authorization : BaseApplicationPlugin { - - /** - * 注册拦截器 - */ - override fun install(pipeline: Application, configure: Unit.() -> Unit): Authorization { - pipeline.intercept(ApplicationCallPipeline.Plugins) { - if (MahContextHolder.singleMode) { - proceed() - return@intercept - } - - val sessionKey = sessionKeyFromHeader() ?: sessionKeyFromAuthorization() - if (sessionKey != null) { - MahContextHolder[sessionKey]?.let { - call.attributes.put(sessionAttr, it) - } - } - - proceed() - } - return this - } - - private fun PipelineContext<*, ApplicationCall>.sessionKeyFromHeader(): String? { - return call.request.header("sessionKey") - } - - private fun PipelineContext<*, ApplicationCall>.sessionKeyFromAuthorization(): String? { - return call.request.header("Authorization")?.run { - val (type, value) = split(' ', limit = 2) - - return if (type.equals("session", ignoreCase = true) || type.equals("sessionKey", ignoreCase = true)) { - value - } else { - null - } - } - } - - override val key: AttributeKey = AttributeKey("Authorization") - - @JvmField - val sessionAttr: AttributeKey = AttributeKey("Session") - - val PipelineContext<*, ApplicationCall>.headerSession: Session? - get() { - return this.call.attributes.getOrNull(sessionAttr) - } -} diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/handler/HttpRouterAccessHandler.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/handler/HttpRouterAccessHandler.kt deleted file mode 100644 index 2a44ee55..00000000 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/feature/handler/HttpRouterAccessHandler.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.api.http.adapter.http.feature.handler - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.plugins.* -import io.ktor.server.request.* -import io.ktor.util.* -import io.ktor.util.pipeline.* -import io.ktor.utils.io.streams.* -import net.mamoe.mirai.api.http.adapter.common.StateCode -import net.mamoe.mirai.api.http.adapter.http.router.respondDTO -import net.mamoe.mirai.api.http.adapter.internal.handler.handleException -import net.mamoe.mirai.utils.MiraiLogger - -class HttpRouterAccessHandler private constructor(configure: Configuration) { - - private val logger = configure.logger.value - private val enableAccessLog = configure.enableAccessLog - - private suspend fun intercept(context: PipelineContext) { - handleException { - context.call.apply { - readBody() - logAccess() - } - context.proceed() - }?.also { - traceStateCode(it) - context.call.respondDTO(it) - } - } - - private suspend fun ApplicationCall.readBody() { - if (request.httpMethod == HttpMethod.Post && !request.isMultipart()) { - - val content = receiveChannel().readRemaining().use { - val charset = request.contentCharset() ?: Charsets.UTF_8 - if (charset == Charsets.UTF_8) it.readText() - else it.inputStream().reader(charset).use { rd -> rd.readText() } - } - - attributes.put(bodyContentAttrKey, content) - } - } - - private fun ApplicationCall.logAccess() { - if (enableAccessLog) { - logger.debug("requesting [${request.origin.version}] [${request.httpMethod.value}] ${request.uri}") - logger.debug("with ${parseRequestParameter()}") - } - } - - private fun ApplicationCall.parseRequestParameter(): String = - when (request.httpMethod) { - HttpMethod.Get -> request.queryString() - HttpMethod.Post -> bodyContent() - else -> "" - } - - private fun traceStateCode(stateCode: StateCode) { - if (stateCode is StateCode.InternalError) { - logger.error(stateCode.throwable) - } - } - - class Configuration { - var logger = lazy { MiraiLogger.Factory.create(HttpRouterAccessHandler::class, "MAH Access") } - var enableAccessLog = false - } - - companion object Feature : BaseApplicationPlugin { - - override val key: AttributeKey = AttributeKey("Http Router Exception Handler") - val bodyContentAttrKey = AttributeKey("Body Content") - - fun ApplicationCall.bodyContent() = attributes.getOrNull(bodyContentAttrKey) ?: "" - - override fun install(pipeline: Application, configure: Configuration.() -> Unit): HttpRouterAccessHandler { - val configuration = Configuration().apply(configure) - val feature = HttpRouterAccessHandler(configuration) - pipeline.intercept(ApplicationCallPipeline.Monitoring) { feature.intercept(this) } - return feature - } - } -} \ No newline at end of file diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/Authorization.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/Authorization.kt new file mode 100644 index 00000000..568bfcc3 --- /dev/null +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/Authorization.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.util.* +import net.mamoe.mirai.api.http.context.MahContextHolder +import net.mamoe.mirai.api.http.context.session.Session + +private val sessionAttr: AttributeKey = AttributeKey("session") +val Authorization = createApplicationPlugin("Authorization") { + + onCall { call -> + if (MahContextHolder.singleMode) { + return@onCall + } + + val sessionKey = call.sessionKeyFromHeader() ?: call.sessionKeyFromAuthorization() + if (sessionKey != null) { + MahContextHolder[sessionKey]?.let { + call.attributes.put(sessionAttr, it) + } + } + } +} + +val ApplicationCall.session: Session? + get() { + return this.attributes.getOrNull(sessionAttr) + } + +private fun ApplicationCall.sessionKeyFromHeader(): String? { + return request.header("sessionKey") +} + +private fun ApplicationCall.sessionKeyFromAuthorization(): String? { + return request.header("Authorization")?.run { + val (type, value) = split(' ', limit = 2) + + return if (type.equals("session", ignoreCase = true) || type.equals("sessionKey", ignoreCase = true)) { + value + } else { + null + } + } +} diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceive.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceive.kt new file mode 100644 index 00000000..5877aa7a --- /dev/null +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceive.kt @@ -0,0 +1,20 @@ +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.server.application.* +import io.ktor.util.* +import io.ktor.utils.io.* + +private val doubleReceiveAttrKey: AttributeKey = AttributeKey("doubleReceiveBody") +val DoubleReceive = createApplicationPlugin("DoubleReceive") { + + on(ReceiveBodyTransformed) { call, body -> + if (body is ByteReadChannel) return@on body + + call.attributes.getOrNull(doubleReceiveAttrKey)?.let { + return@on it + } + + call.attributes.put(doubleReceiveAttrKey, body) + return@on body + } +} \ No newline at end of file diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitor.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitor.kt new file mode 100644 index 00000000..ae768615 --- /dev/null +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitor.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.* +import io.ktor.server.request.* +import net.mamoe.mirai.utils.MiraiLogger + +val logger by lazy { MiraiLogger.Factory.create(HttpRouterMonitor::class, "MAH Access") } +val HttpRouterMonitor = createApplicationPlugin("HttpRouterAccessMonitor") { + on(Monitor) { call -> + call.logAccess() + } +} + +private suspend fun ApplicationCall.logAccess() { + logger.debug("requesting [${request.origin.version}] [${request.httpMethod.value}] ${request.uri}") + if (!request.isMultipart()) { + logger.debug("with ${parseRequestParameter()}") + } +} + +private suspend fun ApplicationCall.parseRequestParameter(): String = + when (request.httpMethod) { + HttpMethod.Get -> request.queryString() + HttpMethod.Post -> receive() + else -> "" + } \ No newline at end of file diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/hooks.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/hooks.kt new file mode 100644 index 00000000..52a02632 --- /dev/null +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/hooks.kt @@ -0,0 +1,24 @@ +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.server.application.* +import io.ktor.server.request.* + +internal object Monitor : Hook Unit> { + override fun install(pipeline: ApplicationCallPipeline, handler: suspend (ApplicationCall) -> Unit) { + pipeline.intercept(ApplicationCallPipeline.Monitoring) { + handler(call) + } + } +} + +internal object ReceiveBodyTransformed : Hook Any> { + override fun install( + pipeline: ApplicationCallPipeline, + handler: suspend (call: ApplicationCall, state: Any) -> Any + ) { + pipeline.receivePipeline.intercept(ApplicationReceivePipeline.After) { + val body = handler(call, it) + proceedWith(body) + } + } +} \ No newline at end of file diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt index b92cb7da..b8803d4d 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt @@ -13,8 +13,9 @@ import io.ktor.server.application.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.defaultheaders.* import net.mamoe.mirai.api.http.adapter.http.HttpAdapter -import net.mamoe.mirai.api.http.adapter.http.feature.auth.Authorization -import net.mamoe.mirai.api.http.adapter.http.feature.handler.HttpRouterAccessHandler +import net.mamoe.mirai.api.http.adapter.http.plugin.Authorization +import net.mamoe.mirai.api.http.adapter.http.plugin.DoubleReceive +import net.mamoe.mirai.api.http.adapter.http.plugin.HttpRouterMonitor import net.mamoe.mirai.api.http.context.MahContextHolder @@ -30,7 +31,10 @@ fun Application.httpModule(adapter: HttpAdapter) { } install(Authorization) - install(HttpRouterAccessHandler) { enableAccessLog = MahContextHolder.debug } + if (MahContextHolder.debug) { + install(DoubleReceive) + install(HttpRouterMonitor) + } authRouter(adapter.setting) messageRouter() diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt index 99824e1f..77f5ec8a 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt @@ -9,9 +9,9 @@ package net.mamoe.mirai.api.http.adapter.http.router -import io.ktor.server.application.* import io.ktor.http.* import io.ktor.http.content.* +import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -22,8 +22,7 @@ import kotlinx.serialization.serializer import net.mamoe.mirai.api.http.adapter.common.IllegalParamException import net.mamoe.mirai.api.http.adapter.common.IllegalSessionException import net.mamoe.mirai.api.http.adapter.common.StateCode -import net.mamoe.mirai.api.http.adapter.http.feature.auth.Authorization.headerSession -import net.mamoe.mirai.api.http.adapter.http.feature.handler.HttpRouterAccessHandler.Feature.bodyContent +import net.mamoe.mirai.api.http.adapter.http.plugin.session import net.mamoe.mirai.api.http.adapter.http.util.KtorParameterFormat import net.mamoe.mirai.api.http.adapter.internal.consts.Paths import net.mamoe.mirai.api.http.adapter.internal.dto.AuthedDTO @@ -140,7 +139,7 @@ internal inline fun Route.httpAuthedMultiPart( * 获取 session 并进行类型校验 */ private fun PipelineContext<*, ApplicationCall>.getAuthedSession(sessionKey: String): Session { - return headerSession ?: MahContextHolder[sessionKey] + return call.session ?: MahContextHolder[sessionKey] ?: throw IllegalSessionException } @@ -169,8 +168,8 @@ internal suspend fun ApplicationCall.respondJson(json: String, status: HttpStatu /** * 接收 http body 指定类型 [T] 的 [DTO] */ -internal inline fun ApplicationCall.receiveDTO(): T? = - bodyContent().jsonParseOrNull() +internal suspend inline fun ApplicationCall.receiveDTO(): T? = + receive().jsonParseOrNull() /** * 接收 http multi part 值类型 diff --git a/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceivePluginTest.kt b/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceivePluginTest.kt new file mode 100644 index 00000000..70639bcc --- /dev/null +++ b/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/DoubleReceivePluginTest.kt @@ -0,0 +1,119 @@ +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import io.ktor.utils.io.streams.* +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertNotNull + +class DoubleReceivePluginTest { + + @Test + fun testDoubleReceive() = testApplication { + install(DoubleReceive) + + val context = "hello world" + + routing { + post("/test") { + val receive = call.receive() + assertEquals(context, receive) + + val doubleReceive = call.receive() + assertEquals(context, doubleReceive) + } + } + + client.post("/test") { + setBody(context) + } + } + + @Test + fun testDoubleReceiveWithoutTransformed() = testApplication { + install(DoubleReceive) + + val context = "hello world" + + routing { + post("/test") { + val receive = call.receiveChannel().readRemaining().use { + it.inputStream().reader(Charsets.UTF_8).use { rd -> rd.readText() } + } + assertEquals(context, receive) + + // double receive will fail + assertFails { + call.receiveChannel().readRemaining().use { + it.inputStream().reader(Charsets.UTF_8).use { rd -> rd.readText() } + } + } + } + } + + client.post("/test") { + setBody(context) + } + } + + @Test + fun testDoubleReceiveWithMultipart() = testApplication { + install(DoubleReceive) + + routing { + post("/testMultipart") { + val part = call.receiveMultipart().readPart() + assertNotNull(part) + + val doubleReceive = call.receiveMultipart().readAllParts() + assertEquals(2, doubleReceive.size) + } + } + + client.submitFormWithBinaryData("/testMultipart", formData { + append("path", "/") + append("type", "group") + append("file", "content", Headers.build { + append(HttpHeaders.ContentDisposition, "filename=upload.txt") + }) + }) + } + + @Serializable + private data class TestDTO(val data: String) + + @Test + fun testDoubleReceiveDifferentType() = testApplication { + install(DoubleReceive) + install(ContentNegotiation) { + json() + } + + val context = """{"data": "hello world"}""" + + routing { + post("/test") { + val receive = call.receive() + assertEquals(context, receive) + + val dto = call.receive() + assertEquals("hello world", dto.data) + } + } + + client.post("/test") { + setBody(context) + } + } +} \ No newline at end of file diff --git a/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitorTest.kt b/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitorTest.kt new file mode 100644 index 00000000..b7f00350 --- /dev/null +++ b/mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/http/plugin/HttpRouterMonitorTest.kt @@ -0,0 +1,36 @@ +package net.mamoe.mirai.api.http.adapter.http.plugin + +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class HttpRouterMonitorTest { + + private data class TestDTO(val data: String) + + @Test + fun testMonitorDoubleReceive() = testApplication { + install(HttpRouterMonitor) + install(DoubleReceive) + install(ContentNegotiation) { + json() + } + + routing { + post("/test") { + val dto = call.receive() + assertEquals("hello world", dto.data) + } + } + + client.post("/test") { + setBody("""{"data":"hello world"}""") + } + } +}