From ff3e2dc4b1cea24ce692c8f289f1406f166839a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 12:15:49 +0800 Subject: [PATCH 1/9] refactor(rest): improve response creation --- .../netty/http/util/ByteBufExtensions.kt | 29 ++++ .../ccbluex/netty/http/util/HttpResponse.kt | 146 +++++++++++------- .../ccbluex/netty/http/util/Serializations.kt | 51 ++++++ 3 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt create mode 100644 src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt b/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt new file mode 100644 index 0000000..271de03 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt @@ -0,0 +1,29 @@ +/* + * This file is part of Netty-Rest (https://github.com/CCBlueX/netty-rest) + * + * Copyright (c) 2024 CCBlueX + * + * LiquidBounce is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Netty-Rest is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Netty-Rest. If not, see . + * + */ +@file:Suppress("NOTHING_TO_INLINE") +package net.ccbluex.netty.http.util + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufInputStream +import io.netty.buffer.ByteBufOutputStream + +inline fun ByteBuf.inputStream() = ByteBufInputStream(this) + +inline fun ByteBuf.outputStream() = ByteBufOutputStream(this) diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt index eec1244..3bc6e64 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -19,29 +19,32 @@ */ package net.ccbluex.netty.http.util -import com.google.gson.Gson import com.google.gson.JsonElement -import com.google.gson.JsonObject -import io.netty.buffer.Unpooled +import io.netty.buffer.ByteBuf +import io.netty.buffer.PooledByteBufAllocator import io.netty.handler.codec.http.* import org.apache.tika.Tika import java.io.File import java.io.InputStream +import java.lang.reflect.Type /** * Creates an HTTP response with the given status, content type, and content. * * @param status The HTTP response status. * @param contentType The content type of the response. Defaults to "text/plain". - * @param content The content of the response. + * @param content The content of the response, in [io.netty.buffer.ByteBuf]. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain", - content: String): FullHttpResponse { +fun httpResponse( + status: HttpResponseStatus, + contentType: String = "text/plain", + content: ByteBuf +): FullHttpResponse { val response = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, status, - Unpooled.wrappedBuffer(content.toByteArray()) + content ) val httpHeaders = response.headers() @@ -51,6 +54,37 @@ fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain", return response } +/** + * Creates an HTTP response with the given status, content type, and content. + * + * @param status The HTTP response status. + * @param contentType The content type of the response. Defaults to "text/plain". + * @param content The content of the response, in [String]. + * @return A FullHttpResponse object. + */ +fun httpResponse( + status: HttpResponseStatus, + contentType: String = "text/plain", + content: String +): FullHttpResponse { + val buf = PooledByteBufAllocator.DEFAULT.buffer(content.length * 3) + buf.writeCharSequence(content, Charsets.UTF_8) + return httpResponse(status, contentType, buf) +} + +/** + * Creates an HTTP response with the given status and JSON content. + * + * @param status The HTTP response status. + * @param json The JSON content of the response. + * @return A FullHttpResponse object. + */ +fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse( + status, + "application/json", + PooledByteBufAllocator.DEFAULT.writeJson(json) +) + /** * Creates an HTTP response with the given status and JSON content. * @@ -58,8 +92,11 @@ fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain", * @param json The JSON content of the response. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, json: JsonElement) - = httpResponse(status, "application/json", Gson().toJson(json)) +fun httpResponse(status: HttpResponseStatus, json: T, type: Type) = httpResponse( + status, + "application/json", + PooledByteBufAllocator.DEFAULT.writeJson(json, type) +) /** * Creates an HTTP 200 OK response with the given JSON content. @@ -67,8 +104,7 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) * @param jsonElement The JSON content of the response. * @return A FullHttpResponse object. */ -fun httpOk(jsonElement: JsonElement) - = httpResponse(HttpResponseStatus.OK, jsonElement) +fun httpOk(jsonElement: JsonElement) = httpResponse(HttpResponseStatus.OK, jsonElement) /** * Creates an HTTP 404 Not Found response with the given path and reason. @@ -78,10 +114,8 @@ fun httpOk(jsonElement: JsonElement) * @return A FullHttpResponse object. */ fun httpNotFound(path: String, reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("path", path) - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.NOT_FOUND, jsonObject) + data class ResponseBody(val path: String, val reason: String) + return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason), ResponseBody::class.java) } /** @@ -91,9 +125,8 @@ fun httpNotFound(path: String, reason: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpInternalServerError(exception: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", exception) - return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception), ResponseBody::class.java) } /** @@ -103,9 +136,8 @@ fun httpInternalServerError(exception: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpForbidden(reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.FORBIDDEN, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason), ResponseBody::class.java) } /** @@ -115,9 +147,8 @@ fun httpForbidden(reason: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpBadRequest(reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.BAD_REQUEST, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason), ResponseBody::class.java) } private val tika = Tika() @@ -129,16 +160,21 @@ private val tika = Tika() * @return A FullHttpResponse object. */ fun httpFile(file: File): FullHttpResponse { - val response = DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.OK, - Unpooled.wrappedBuffer(file.readBytes()) - ) + val buf = file.inputStream().channel.use { channel -> + val size = channel.size().toInt() + val buf = PooledByteBufAllocator.DEFAULT.buffer(size) + while (buf.writableBytes() > 0) { + val written = buf.writeBytes(channel, buf.writableBytes()) + if (written <= 0) break + } + buf + } - val httpHeaders = response.headers() - httpHeaders[HttpHeaderNames.CONTENT_TYPE] = tika.detect(file) - httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes() - return response + return httpResponse( + status = HttpResponseStatus.OK, + contentType = tika.detect(file), + content = buf + ) } /** @@ -148,19 +184,21 @@ fun httpFile(file: File): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpFileStream(stream: InputStream): FullHttpResponse { - val bytes = stream.readBytes() + val allocator = PooledByteBufAllocator.DEFAULT + val buf = allocator.buffer() - val response = DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.OK, - Unpooled.wrappedBuffer(bytes) - ) - - val httpHeaders = response.headers() - httpHeaders[HttpHeaderNames.CONTENT_TYPE] = tika.detect(bytes) - httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes() + val tmp = ByteArray(8192) + while (true) { + val read = stream.read(tmp) + if (read == -1) break + buf.writeBytes(tmp, 0, read) + } - return response + return httpResponse( + status = HttpResponseStatus.OK, + contentType = tika.detect(buf.duplicate().inputStream()), + content = buf + ) } /** @@ -186,9 +224,8 @@ fun httpNoContent(): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpMethodNotAllowed(method: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("method", method) - return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, jsonObject) + data class ResponseBody(val method: String) + return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method), ResponseBody::class.java) } /** @@ -198,9 +235,8 @@ fun httpMethodNotAllowed(method: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpUnauthorized(reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.UNAUTHORIZED, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason), ResponseBody::class.java) } /** @@ -210,9 +246,8 @@ fun httpUnauthorized(reason: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpTooManyRequests(reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason), ResponseBody::class.java) } /** @@ -222,7 +257,6 @@ fun httpTooManyRequests(reason: String): FullHttpResponse { * @return A FullHttpResponse object. */ fun httpServiceUnavailable(reason: String): FullHttpResponse { - val jsonObject = JsonObject() - jsonObject.addProperty("reason", reason) - return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, jsonObject) + data class ResponseBody(val reason: String) + return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason), ResponseBody::class.java) } diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt new file mode 100644 index 0000000..f323baf --- /dev/null +++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt @@ -0,0 +1,51 @@ +/* + * This file is part of Netty-Rest (https://github.com/CCBlueX/netty-rest) + * + * Copyright (c) 2024 CCBlueX + * + * LiquidBounce is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Netty-Rest is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Netty-Rest. If not, see . + * + */ +package net.ccbluex.netty.http.util + +import com.google.gson.Gson +import com.google.gson.JsonElement +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import java.lang.reflect.Type + +private val gson = Gson() + +fun ByteBufAllocator.writeJson( + json: JsonElement, +): ByteBuf { + val buf = buffer(256, Int.MAX_VALUE) + gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer -> + gson.toJson(json, writer) + writer.flush() + } + return buf +} + +fun ByteBufAllocator.writeJson( + obj: T, + type: Type +): ByteBuf { + val buf = buffer(256, Int.MAX_VALUE) + gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer -> + gson.toJson(obj, type, writer) + writer.flush() + } + return buf +} From 7630c5fa6710f982a33979d38d9fbd4d633e9cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 13:08:09 +0800 Subject: [PATCH 2/9] shared GSON --- .../kotlin/net/ccbluex/netty/http/model/RequestObject.kt | 9 +++++++-- .../kotlin/net/ccbluex/netty/http/util/Serializations.kt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt b/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt index cd73ad0..77c935f 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt @@ -19,8 +19,8 @@ */ package net.ccbluex.netty.http.model -import com.google.gson.Gson import io.netty.handler.codec.http.HttpMethod +import net.ccbluex.netty.http.util.gson /** * Represents an HTTP request object. @@ -51,7 +51,12 @@ data class RequestObject( * @return The JSON object of the specified type. */ inline fun asJson(): T { - return Gson().fromJson(body, T::class.java) + return GSON_INSTANCE.fromJson(body, T::class.java) + } + + companion object { + @JvmField + val GSON_INSTANCE = gson } } \ No newline at end of file diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt index f323baf..4a87c23 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt @@ -25,7 +25,7 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBufAllocator import java.lang.reflect.Type -private val gson = Gson() +internal val gson = Gson() fun ByteBufAllocator.writeJson( json: JsonElement, From f6d446f60d112939ef1c6131fa1450e7de2b165a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 13:08:31 +0800 Subject: [PATCH 3/9] other stuffs --- .../net/ccbluex/netty/http/HttpConductor.kt | 2 +- .../ccbluex/netty/http/HttpServerHandler.kt | 2 +- .../netty/http/model/RequestContext.kt | 35 ++++++++----------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt index f4a968f..11baa89 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt @@ -52,7 +52,7 @@ internal class HttpConductor(private val server: HttpServer) { val response = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, - Unpooled.wrappedBuffer(ByteArray(0)) + Unpooled.EMPTY_BUFFER ) val httpHeaders = response.headers() diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt index 031659f..546e4f0 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt @@ -86,7 +86,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun } else { val requestContext = RequestContext( msg.method(), - URLDecoder.decode(msg.uri(), "UTF-8"), + URLDecoder.decode(msg.uri(), Charsets.UTF_8), msg.headers().associate { it.key to it.value }, ) diff --git a/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt b/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt index cf5bb6e..608902d 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt @@ -20,12 +20,10 @@ package net.ccbluex.netty.http.model import io.netty.handler.codec.http.HttpMethod -import java.util.* -import java.util.stream.Collectors data class RequestContext(var httpMethod: HttpMethod, var uri: String, var headers: Map) { val contentBuffer = StringBuilder() - val path = if (uri.contains("?")) uri.substring(0, uri.indexOf('?')) else uri + val path = uri.substringBefore('?', uri) val params = getUriParams(uri) } @@ -33,24 +31,21 @@ data class RequestContext(var httpMethod: HttpMethod, var uri: String, var heade * The received uri should be like: '...?param1=value¶m2=value' */ private fun getUriParams(uri: String): Map { - if (uri.contains("?")) { - val paramsString = uri.substring(uri.indexOf('?') + 1) + val queryString = uri.substringAfter('?', "") - // in case of duplicated params, will be used la last value - return Arrays.stream( - if (paramsString.contains("&")) paramsString.split("&".toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray() else arrayOf(paramsString)) - .map { value: String -> - value.split("=".toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray() - } - .collect( - Collectors.toMap( - { paramValue: Array -> paramValue[0] }, - { paramValue: Array -> paramValue[1] }, - { v1: String?, v2: String -> v2 }) - ) + if (queryString.isEmpty()) { + return emptyMap() } - return emptyMap() + // in case of duplicated params, will be used the last value + return queryString.split('&') + .mapNotNull { param -> + val index = param.indexOf('=') + if (index == -1) null + else { + val key = param.substring(0, index) + val value = param.substring(index + 1) + if (key.isNotEmpty()) key to value else null + } + }.toMap() } From b649d26f8ec67768b696e1f7828b5d940ba90784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 17:21:59 +0800 Subject: [PATCH 4/9] small optimizations --- .../net/ccbluex/netty/http/HttpConductor.kt | 19 ++++++------------- .../ccbluex/netty/http/HttpServerHandler.kt | 5 ++--- .../ccbluex/netty/http/rest/RouteControl.kt | 8 ++++++-- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt index 11baa89..17e6568 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt @@ -27,6 +27,8 @@ import net.ccbluex.netty.http.util.httpBadRequest import net.ccbluex.netty.http.util.httpInternalServerError import net.ccbluex.netty.http.util.httpNotFound import net.ccbluex.netty.http.model.RequestObject +import net.ccbluex.netty.http.util.httpNoContent +import net.ccbluex.netty.http.util.httpResponse internal class HttpConductor(private val server: HttpServer) { @@ -48,22 +50,13 @@ internal class HttpConductor(private val server: HttpServer) { return@runCatching httpBadRequest("Incomplete request") } - if (method == HttpMethod.OPTIONS) { - val response = DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.OK, - Unpooled.EMPTY_BUFFER - ) - - val httpHeaders = response.headers() - httpHeaders[HttpHeaderNames.CONTENT_TYPE] = "text/plain" - httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes() - return@runCatching response - } - val (node, params, remaining) = server.routeController.processPath(context.path, method) ?: return@runCatching httpNotFound(context.path, "Route not found") + if (method == HttpMethod.OPTIONS) { + return@runCatching httpNoContent() + } + logger.debug("Found destination {}", node) val requestObject = RequestObject( uri = context.uri, diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt index 546e4f0..b6e2f12 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt @@ -95,13 +95,12 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun } is HttpContent -> { - if (localRequestContext.get() == null) { + val requestContext = localRequestContext.get() ?: run { logger.warn("Received HttpContent without HttpRequest") return } // Append content to the buffer - val requestContext = localRequestContext.get() requestContext .contentBuffer .append(msg.content().toString(Charsets.UTF_8)) @@ -109,7 +108,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun // If this is the last content, process the request if (msg is LastHttpContent) { localRequestContext.remove() - + val httpConductor = HttpConductor(server) val response = httpConductor.processRequestContext(requestContext) val httpResponse = server.middlewares.fold(response) { acc, f -> f(requestContext, acc) } diff --git a/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt b/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt index 84b5bc5..54a752d 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt @@ -55,7 +55,7 @@ class RouteController : Node("") { */ internal fun processPath(path: String, method: HttpMethod): Destination? { val pathArray = path.asPathArray() - .also { if (it.isEmpty()) throw IllegalArgumentException("Path cannot be empty") } + require(pathArray.isNotEmpty()) { "Path cannot be empty" } return travelNode(this, pathArray, method, 0, mutableMapOf()) } @@ -282,4 +282,8 @@ class FileServant(part: String, private val baseFolder: File) : Node(part) { * * @return An array of path parts. */ -private fun String.asPathArray() = split("/").drop(1).toTypedArray() \ No newline at end of file +private fun String.asPathArray(): Array { + val parts = split("/") + return if (parts.size <= 1) emptyArray() + else parts.subList(1, parts.size).toTypedArray() +} \ No newline at end of file From f02c81e2d36dfb2aaa87fa30485bc8755300d01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 17:23:10 +0800 Subject: [PATCH 5/9] imports --- src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt index 17e6568..69d1e90 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt @@ -19,7 +19,6 @@ */ package net.ccbluex.netty.http -import io.netty.buffer.Unpooled import io.netty.handler.codec.http.* import net.ccbluex.netty.http.HttpServer.Companion.logger import net.ccbluex.netty.http.model.RequestContext @@ -28,7 +27,6 @@ import net.ccbluex.netty.http.util.httpInternalServerError import net.ccbluex.netty.http.util.httpNotFound import net.ccbluex.netty.http.model.RequestObject import net.ccbluex.netty.http.util.httpNoContent -import net.ccbluex.netty.http.util.httpResponse internal class HttpConductor(private val server: HttpServer) { From 30f9603b2321751c853df4236a2fff01a56f3d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 17:27:15 +0800 Subject: [PATCH 6/9] remove type param --- .../ccbluex/netty/http/util/HttpResponse.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt index 3bc6e64..503bf30 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -26,7 +26,6 @@ import io.netty.handler.codec.http.* import org.apache.tika.Tika import java.io.File import java.io.InputStream -import java.lang.reflect.Type /** * Creates an HTTP response with the given status, content type, and content. @@ -92,10 +91,10 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse( * @param json The JSON content of the response. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, json: T, type: Type) = httpResponse( +fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse( status, "application/json", - PooledByteBufAllocator.DEFAULT.writeJson(json, type) + PooledByteBufAllocator.DEFAULT.writeJson(json, (json as Any).javaClass) ) /** @@ -115,7 +114,7 @@ fun httpOk(jsonElement: JsonElement) = httpResponse(HttpResponseStatus.OK, jsonE */ fun httpNotFound(path: String, reason: String): FullHttpResponse { data class ResponseBody(val path: String, val reason: String) - return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason)) } /** @@ -126,7 +125,7 @@ fun httpNotFound(path: String, reason: String): FullHttpResponse { */ fun httpInternalServerError(exception: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception)) } /** @@ -137,7 +136,7 @@ fun httpInternalServerError(exception: String): FullHttpResponse { */ fun httpForbidden(reason: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason)) } /** @@ -148,7 +147,7 @@ fun httpForbidden(reason: String): FullHttpResponse { */ fun httpBadRequest(reason: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason)) } private val tika = Tika() @@ -225,7 +224,7 @@ fun httpNoContent(): FullHttpResponse { */ fun httpMethodNotAllowed(method: String): FullHttpResponse { data class ResponseBody(val method: String) - return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method)) } /** @@ -236,7 +235,7 @@ fun httpMethodNotAllowed(method: String): FullHttpResponse { */ fun httpUnauthorized(reason: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason)) } /** @@ -247,7 +246,7 @@ fun httpUnauthorized(reason: String): FullHttpResponse { */ fun httpTooManyRequests(reason: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason)) } /** @@ -258,5 +257,5 @@ fun httpTooManyRequests(reason: String): FullHttpResponse { */ fun httpServiceUnavailable(reason: String): FullHttpResponse { data class ResponseBody(val reason: String) - return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason), ResponseBody::class.java) + return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason)) } From c0b708555d6036556cc3b74c012933f4552c449a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 17:31:54 +0800 Subject: [PATCH 7/9] comments --- .../net/ccbluex/netty/http/util/HttpResponse.kt | 4 ++-- .../net/ccbluex/netty/http/util/Serializations.kt | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt index 503bf30..5b6de70 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -91,10 +91,10 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse( * @param json The JSON content of the response. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse( +fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse( status, "application/json", - PooledByteBufAllocator.DEFAULT.writeJson(json, (json as Any).javaClass) + PooledByteBufAllocator.DEFAULT.writeJson(json, json.javaClass) ) /** diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt index 4a87c23..4ff8280 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt @@ -27,6 +27,9 @@ import java.lang.reflect.Type internal val gson = Gson() +/** + * Serialize [json] into [ByteBuf] with given [ByteBufAllocator]. + */ fun ByteBufAllocator.writeJson( json: JsonElement, ): ByteBuf { @@ -38,9 +41,13 @@ fun ByteBufAllocator.writeJson( return buf } -fun ByteBufAllocator.writeJson( +/** + * Serialize [obj] as [type] into [ByteBuf] with given [ByteBufAllocator]. + */ +@JvmOverloads +fun ByteBufAllocator.writeJson( obj: T, - type: Type + type: Type = obj.javaClass, ): ByteBuf { val buf = buffer(256, Int.MAX_VALUE) gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer -> From 525099e5b59c018b6c64ad62c0dad807f831fe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 21:34:10 +0800 Subject: [PATCH 8/9] improve file stream response --- .../ccbluex/netty/http/util/HttpResponse.kt | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt index 5b6de70..020e87e 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -25,6 +25,7 @@ import io.netty.buffer.PooledByteBufAllocator import io.netty.handler.codec.http.* import org.apache.tika.Tika import java.io.File +import java.io.IOException import java.io.InputStream /** @@ -159,20 +160,9 @@ private val tika = Tika() * @return A FullHttpResponse object. */ fun httpFile(file: File): FullHttpResponse { - val buf = file.inputStream().channel.use { channel -> - val size = channel.size().toInt() - val buf = PooledByteBufAllocator.DEFAULT.buffer(size) - while (buf.writableBytes() > 0) { - val written = buf.writeBytes(channel, buf.writableBytes()) - if (written <= 0) break - } - buf - } - - return httpResponse( - status = HttpResponseStatus.OK, + return httpFileStream( + file.inputStream(), contentType = tika.detect(file), - content = buf ) } @@ -180,24 +170,36 @@ fun httpFile(file: File): FullHttpResponse { * Creates an HTTP response for the given input stream. * * @param stream The input stream to be included in the response. + * It will be closed after reading. + * @param contentType The content type of [stream]. + * Defaults to `null`, which means to auto-detect by [Tika]. * @return A FullHttpResponse object. */ -fun httpFileStream(stream: InputStream): FullHttpResponse { +@JvmOverloads +fun httpFileStream( + stream: InputStream, + contentType: String? = null +): FullHttpResponse { val allocator = PooledByteBufAllocator.DEFAULT val buf = allocator.buffer() - val tmp = ByteArray(8192) - while (true) { - val read = stream.read(tmp) - if (read == -1) break - buf.writeBytes(tmp, 0, read) - } + try { + stream.use { + while (true) { + val read = buf.writeBytes(stream, 8192) + if (read == -1) break + } + } - return httpResponse( - status = HttpResponseStatus.OK, - contentType = tika.detect(buf.duplicate().inputStream()), - content = buf - ) + return httpResponse( + status = HttpResponseStatus.OK, + contentType = contentType ?: tika.detect(buf.duplicate().inputStream()), + content = buf + ) + } catch (e: IOException) { + buf.release() + return httpInternalServerError(e.stackTraceToString()) + } } /** From 69a00cf46661345f5eb8194c9cec5367ede6f918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 20 May 2025 21:40:35 +0800 Subject: [PATCH 9/9] add checks --- .../net/ccbluex/netty/http/util/HttpResponse.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt index 020e87e..7d52945 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -160,28 +160,33 @@ private val tika = Tika() * @return A FullHttpResponse object. */ fun httpFile(file: File): FullHttpResponse { + require(file.length() <= Int.MAX_VALUE) { "File is too big" } + return httpFileStream( file.inputStream(), contentType = tika.detect(file), + contentLength = file.length().toInt() ) } /** * Creates an HTTP response for the given input stream. * - * @param stream The input stream to be included in the response. - * It will be closed after reading. - * @param contentType The content type of [stream]. - * Defaults to `null`, which means to auto-detect by [Tika]. + * @param stream The input stream to be included in the response. It will be closed after reading. + * @param contentType The content type of [stream]. Defaults to `null`, which means to auto-detect by [Tika]. + * @param contentLength The predicated content length of [stream]. Defaults to `256`. * @return A FullHttpResponse object. */ @JvmOverloads fun httpFileStream( stream: InputStream, - contentType: String? = null + contentType: String? = null, + contentLength: Int = 256, ): FullHttpResponse { + require(contentLength > 0) { "content length must be positive" } + val allocator = PooledByteBufAllocator.DEFAULT - val buf = allocator.buffer() + val buf = allocator.buffer(contentLength) try { stream.use {