From 72e84df21b85de47470f3fe9bd4ed97ed84dc157 Mon Sep 17 00:00:00 2001 From: MukjepScarlet <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:57:22 +0800 Subject: [PATCH 1/5] Overloads for JSON response with custom Gson --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../ccbluex/netty/http/model/RequestObject.kt | 4 +-- .../ccbluex/netty/http/util/HttpResponse.kt | 26 ++++++++++++++++--- .../ccbluex/netty/http/util/Serializations.kt | 15 ++++++----- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6fc2aa6..951d080 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Sep 02 03:41:15 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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 77c935f..2f690d9 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt @@ -20,7 +20,7 @@ package net.ccbluex.netty.http.model import io.netty.handler.codec.http.HttpMethod -import net.ccbluex.netty.http.util.gson +import net.ccbluex.netty.http.util.DEFAULT_GSON /** * Represents an HTTP request object. @@ -56,7 +56,7 @@ data class RequestObject( companion object { @JvmField - val GSON_INSTANCE = gson + val GSON_INSTANCE = DEFAULT_GSON } } \ No newline at end of file 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 7d52945..e1efb75 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt @@ -19,6 +19,7 @@ */ 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.PooledByteBufAllocator @@ -77,12 +78,14 @@ fun httpResponse( * * @param status The HTTP response status. * @param json The JSON content of the response. + * @param gson The Gson instance to serialize the body. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse( +@JvmOverloads +fun httpResponse(status: HttpResponseStatus, json: JsonElement, gson: Gson = DEFAULT_GSON) = httpResponse( status, "application/json", - PooledByteBufAllocator.DEFAULT.writeJson(json) + PooledByteBufAllocator.DEFAULT.writeJson(json, gson) ) /** @@ -90,9 +93,11 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse( * * @param status The HTTP response status. * @param json The JSON content of the response. + * @param gson The Gson instance to serialize the body. * @return A FullHttpResponse object. */ -fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse( +@JvmOverloads +fun httpResponse(status: HttpResponseStatus, json: T, gson: Gson = DEFAULT_GSON) = httpResponse( status, "application/json", PooledByteBufAllocator.DEFAULT.writeJson(json, json.javaClass) @@ -104,7 +109,20 @@ fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse( * @param jsonElement The JSON content of the response. * @return A FullHttpResponse object. */ -fun httpOk(jsonElement: JsonElement) = httpResponse(HttpResponseStatus.OK, jsonElement) +@JvmOverloads +fun httpOk(jsonElement: JsonElement, gson: Gson = DEFAULT_GSON) = + httpResponse(HttpResponseStatus.OK, jsonElement, gson) + +/** + * Creates an HTTP 200 OK response with the given JSON content. + * + * @param json The JSON content of the response. + * @param gson The Gson instance to serialize the body. + * @return A FullHttpResponse object. + */ +@JvmOverloads +fun httpOk(json: T, gson: Gson = DEFAULT_GSON) = + httpResponse(HttpResponseStatus.OK, json, gson) /** * Creates an HTTP 404 Not Found response with the given path and reason. 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 4ff8280..0693fa1 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt @@ -25,18 +25,19 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBufAllocator import java.lang.reflect.Type -internal val gson = Gson() +internal val DEFAULT_GSON = Gson() /** * Serialize [json] into [ByteBuf] with given [ByteBufAllocator]. */ +@JvmOverloads fun ByteBufAllocator.writeJson( json: JsonElement, + gson: Gson = DEFAULT_GSON, ): ByteBuf { val buf = buffer(256, Int.MAX_VALUE) - gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer -> - gson.toJson(json, writer) - writer.flush() + buf.outputStream().writer(Charsets.UTF_8).use { + gson.toJson(json, it) } return buf } @@ -48,11 +49,11 @@ fun ByteBufAllocator.writeJson( fun ByteBufAllocator.writeJson( obj: T, type: Type = obj.javaClass, + gson: Gson = DEFAULT_GSON, ): 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() + buf.outputStream().writer(Charsets.UTF_8).use { + gson.toJson(obj, type, it) } return buf } From d5fd338509280b1f225ad1023791fc4840d5eda8 Mon Sep 17 00:00:00 2001 From: MukjepScarlet <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:35:55 +0800 Subject: [PATCH 2/5] Auto detect Epoll and KQueue --- .../net/ccbluex/netty/http/HttpServer.kt | 14 +-- .../netty/http/util/TransportDetector.kt | 88 +++++++++++++++++++ 2 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt index 2f8ce2b..8eed665 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt @@ -21,15 +21,11 @@ package net.ccbluex.netty.http import io.netty.bootstrap.ServerBootstrap import io.netty.channel.ChannelOption -import io.netty.channel.epoll.Epoll -import io.netty.channel.epoll.EpollEventLoopGroup -import io.netty.channel.epoll.EpollServerSocketChannel -import io.netty.channel.nio.NioEventLoopGroup -import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.handler.logging.LogLevel import io.netty.handler.logging.LoggingHandler import net.ccbluex.netty.http.middleware.Middleware import net.ccbluex.netty.http.rest.RouteController +import net.ccbluex.netty.http.util.TransportType import net.ccbluex.netty.http.websocket.WebSocketController import org.apache.logging.log4j.LogManager @@ -58,15 +54,13 @@ class HttpServer { * Starts the Netty server on the specified port. */ fun start(port: Int) { - val bossGroup = if (Epoll.isAvailable()) EpollEventLoopGroup() else NioEventLoopGroup() - val workerGroup = if (Epoll.isAvailable()) EpollEventLoopGroup() else NioEventLoopGroup() + val b = ServerBootstrap() + + val (bossGroup, workerGroup) = TransportType.apply(b) try { logger.info("Starting Netty server...") - val b = ServerBootstrap() b.option(ChannelOption.SO_BACKLOG, 1024) - b.group(bossGroup, workerGroup) - .channel(if (Epoll.isAvailable()) EpollServerSocketChannel::class.java else NioServerSocketChannel::class.java) .handler(LoggingHandler(LogLevel.INFO)) .childHandler(HttpChannelInitializer(this)) val ch = b.bind(port).sync().channel() diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt b/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt new file mode 100644 index 0000000..f13700e --- /dev/null +++ b/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt @@ -0,0 +1,88 @@ +/* + * 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 io.netty.bootstrap.ServerBootstrap +import io.netty.channel.EventLoopGroup +import io.netty.channel.ServerChannel +import io.netty.channel.epoll.EpollEventLoopGroup +import io.netty.channel.epoll.EpollServerSocketChannel +import io.netty.channel.kqueue.KQueueEventLoopGroup +import io.netty.channel.kqueue.KQueueServerSocketChannel +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.nio.NioServerSocketChannel + +internal sealed interface TransportType { + val isAvailable: Boolean + + fun newParentGroup(): EventLoopGroup + + fun newChildGroup(): EventLoopGroup + + fun newServerChannel(): ServerChannel + + object Nio : TransportType { + override val isAvailable get() = true + + override fun newParentGroup() = NioEventLoopGroup(1) + + override fun newChildGroup() = NioEventLoopGroup() + + override fun newServerChannel() = NioServerSocketChannel() + } + + object Epoll : TransportType { + override val isAvailable get() = try { + io.netty.channel.epoll.Epoll.isAvailable() + } catch (t: Throwable) { false } + + override fun newParentGroup() = EpollEventLoopGroup(1) + + override fun newChildGroup() = EpollEventLoopGroup() + + override fun newServerChannel() = EpollServerSocketChannel() + } + + object KQueue : TransportType { + override val isAvailable get() = try { + io.netty.channel.kqueue.KQueue.isAvailable() + } catch (t: Throwable) { false } + + override fun newParentGroup() = KQueueEventLoopGroup(1) + + override fun newChildGroup() = KQueueEventLoopGroup() + + override fun newServerChannel() = KQueueServerSocketChannel() + } + + companion object { + /** + * @return Parent and child group + */ + fun apply(bootstrap: ServerBootstrap): Pair { + val transportType = arrayOf(Epoll, KQueue, Nio).first { it.isAvailable } + val parentGroup = transportType.newParentGroup() + val childGroup = transportType.newChildGroup() + bootstrap.group(parentGroup, childGroup) + .channelFactory(transportType::newServerChannel) + return parentGroup to childGroup + } + } +} From 1c53c9d9b0bcce6099b31d3cc2e77cf69c319d99 Mon Sep 17 00:00:00 2001 From: MukjepScarlet <93977077+mukjepscarlet@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:55:28 +0800 Subject: [PATCH 3/5] Improve Base64 --- .../net/ccbluex/netty/http/util/Base64.kt | 23 +++++++++++----- .../netty/http/util/TransportDetector.kt | 16 ++++++++---- src/test/kotlin/HttpMiddlewareServerTest.kt | 2 -- src/test/kotlin/util/Base64Test.kt | 26 +++++++++++++++++++ 4 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 src/test/kotlin/util/Base64Test.kt diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Base64.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Base64.kt index 11e9982..28d47e7 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/Base64.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/Base64.kt @@ -19,18 +19,27 @@ */ package net.ccbluex.netty.http.util +import io.netty.buffer.Unpooled +import io.netty.handler.codec.base64.Base64 +import java.nio.channels.FileChannel +import java.nio.file.Files import java.nio.file.Path -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.io.path.readBytes /** * Reads the image at the given [path] and returns it as a base64 encoded string. */ -fun readImageAsBase64(path: Path): String = path.readBytes().encodeBase64() +@Deprecated( + "Use Path.readAsBase64() instead", + ReplaceWith("path.readAsBase64()") +) +fun readImageAsBase64(path: Path): String = path.readAsBase64() /** - * Encodes the byte array to a base64 encoded string. + * Reads the file and returns it as a base64 encoded string. */ -@OptIn(ExperimentalEncodingApi::class) -private fun ByteArray.encodeBase64() = Base64.encode(this) +fun Path.readAsBase64(): String { + return FileChannel.open(this).use { channel -> + val byteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, Files.size(this)) + Base64.encode(Unpooled.wrappedBuffer(byteBuffer), false) + }.toString(Charsets.UTF_8) +} diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt b/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt index f13700e..5c7f485 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt @@ -73,15 +73,21 @@ internal sealed interface TransportType { } companion object { + private val available by lazy { + arrayOf(Epoll, KQueue, Nio).first { it.isAvailable } + } + /** - * @return Parent and child group + * Set the channel factory and event loop groups for the given server bootstrap. + * + * @param bootstrap The server bootstrap to configure. + * @return Parent and child group. */ fun apply(bootstrap: ServerBootstrap): Pair { - val transportType = arrayOf(Epoll, KQueue, Nio).first { it.isAvailable } - val parentGroup = transportType.newParentGroup() - val childGroup = transportType.newChildGroup() + val parentGroup = available.newParentGroup() + val childGroup = available.newChildGroup() bootstrap.group(parentGroup, childGroup) - .channelFactory(transportType::newServerChannel) + .channelFactory(available::newServerChannel) return parentGroup to childGroup } } diff --git a/src/test/kotlin/HttpMiddlewareServerTest.kt b/src/test/kotlin/HttpMiddlewareServerTest.kt index f21367b..bbbfff8 100644 --- a/src/test/kotlin/HttpMiddlewareServerTest.kt +++ b/src/test/kotlin/HttpMiddlewareServerTest.kt @@ -7,8 +7,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.junit.jupiter.api.* -import java.io.File -import java.nio.file.Files import kotlin.concurrent.thread import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/src/test/kotlin/util/Base64Test.kt b/src/test/kotlin/util/Base64Test.kt new file mode 100644 index 0000000..965b172 --- /dev/null +++ b/src/test/kotlin/util/Base64Test.kt @@ -0,0 +1,26 @@ +package util + +import net.ccbluex.netty.http.util.readAsBase64 +import java.nio.file.Files +import java.nio.file.Path +import java.util.Base64 +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +class Base64Test { + + @Test + fun `readAsBase64 should return correct Base64 string`() { + val content = Random.nextBytes(128) + val tempFile: Path = Files.createTempFile("test-image", ".bin") + Files.write(tempFile, content) + + val resultBase64 = tempFile.readAsBase64() + + val expectedBase64 = Base64.getEncoder().encodeToString(content) + assertEquals(expectedBase64, resultBase64) + Files.deleteIfExists(tempFile) + } + +} \ No newline at end of file From 27befa8e07ea1c9ef299658ed63be9c805ebac4e 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, 2 Sep 2025 23:13:43 +0800 Subject: [PATCH 4/5] fix set --- .../net/ccbluex/netty/http/HttpConductor.kt | 73 +++++++++---------- .../net/ccbluex/netty/http/HttpServer.kt | 4 +- .../ccbluex/netty/http/HttpServerHandler.kt | 3 +- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt index 69d1e90..8e53b8d 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt @@ -28,50 +28,45 @@ import net.ccbluex.netty.http.util.httpNotFound import net.ccbluex.netty.http.model.RequestObject import net.ccbluex.netty.http.util.httpNoContent -internal class HttpConductor(private val server: HttpServer) { - - /** - * Processes the incoming request context and returns the response. - * - * @param context The request context to process. - * @return The response to the request. - */ - fun processRequestContext(context: RequestContext) = runCatching { - val content = context.contentBuffer.toString() - val method = context.httpMethod - - logger.debug("Request {}", context) - - if (!context.headers["content-length"].isNullOrEmpty() && - context.headers["content-length"]?.toInt() != content.toByteArray(Charsets.UTF_8).size) { - logger.warn("Received incomplete request: $context") - return@runCatching httpBadRequest("Incomplete request") - } +/** + * Processes the incoming request context and returns the response. + * + * @param context The request context to process. + * @return The response to the request. + */ +fun HttpServer.processRequestContext(context: RequestContext) = runCatching { + val content = context.contentBuffer.toString() + val method = context.httpMethod - val (node, params, remaining) = server.routeController.processPath(context.path, method) ?: - return@runCatching httpNotFound(context.path, "Route not found") + logger.debug("Request {}", context) - if (method == HttpMethod.OPTIONS) { - return@runCatching httpNoContent() - } + if (!context.headers["content-length"].isNullOrEmpty() && + context.headers["content-length"]?.toInt() != content.toByteArray(Charsets.UTF_8).size) { + logger.warn("Received incomplete request: $context") + return@runCatching httpBadRequest("Incomplete request") + } - logger.debug("Found destination {}", node) - val requestObject = RequestObject( - uri = context.uri, - path = context.path, - remainingPath = remaining, - method = method, - body = content, - params = params, - queryParams = context.params, - headers = context.headers - ) + val (node, params, remaining) = routeController.processPath(context.path, method) ?: + return@runCatching httpNotFound(context.path, "Route not found") - return@runCatching node.handleRequest(requestObject) - }.getOrElse { - logger.error("Error while processing request object: $context", it) - httpInternalServerError(it.message ?: "Unknown error") + if (method == HttpMethod.OPTIONS) { + return@runCatching httpNoContent() } + logger.debug("Found destination {}", node) + val requestObject = RequestObject( + uri = context.uri, + path = context.path, + remainingPath = remaining, + method = method, + body = content, + params = params, + queryParams = context.params, + headers = context.headers + ) + return@runCatching node.handleRequest(requestObject) +}.getOrElse { + logger.error("Error while processing request object: $context", it) + httpInternalServerError(it.message ?: "Unknown error") } diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt index 28ae78f..ef452e4 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt @@ -71,7 +71,9 @@ class HttpServer { fun start(port: Int): Int = lock.withLock { val b = ServerBootstrap() - val (bossGroup, workerGroup) = TransportType.apply(b) + val groups = TransportType.apply(b) + bossGroup = groups.first + workerGroup = groups.second try { logger.info("Starting Netty server...") diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt index b6e2f12..712bc66 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt @@ -109,8 +109,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun if (msg is LastHttpContent) { localRequestContext.remove() - val httpConductor = HttpConductor(server) - val response = httpConductor.processRequestContext(requestContext) + val response = server.processRequestContext(requestContext) val httpResponse = server.middlewares.fold(response) { acc, f -> f(requestContext, acc) } ctx.writeAndFlush(httpResponse) } From cea1c5943694fdbb9ac435e4ac440606e4dc20fe 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, 2 Sep 2025 23:14:40 +0800 Subject: [PATCH 5/5] internal --- src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt index 8e53b8d..1614041 100644 --- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt +++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt @@ -34,7 +34,7 @@ import net.ccbluex.netty.http.util.httpNoContent * @param context The request context to process. * @return The response to the request. */ -fun HttpServer.processRequestContext(context: RequestContext) = runCatching { +internal fun HttpServer.processRequestContext(context: RequestContext) = runCatching { val content = context.contentBuffer.toString() val method = context.httpMethod