Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
73 changes: 34 additions & 39 deletions src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
internal 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")
}
16 changes: 6 additions & 10 deletions src/main/kotlin/net/ccbluex/netty/http/HttpServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@ import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
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
import java.net.InetSocketAddress
Expand Down Expand Up @@ -73,15 +69,15 @@ class HttpServer {
* @return actual port of server.
*/
fun start(port: Int): Int = lock.withLock {
bossGroup = if (Epoll.isAvailable()) EpollEventLoopGroup(1) else NioEventLoopGroup(1)
workerGroup = if (Epoll.isAvailable()) EpollEventLoopGroup() else NioEventLoopGroup()
val b = ServerBootstrap()

val groups = TransportType.apply(b)
bossGroup = groups.first
workerGroup = groups.second

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()
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -56,7 +56,7 @@ data class RequestObject(

companion object {
@JvmField
val GSON_INSTANCE = gson
val GSON_INSTANCE = DEFAULT_GSON
}

}
23 changes: 16 additions & 7 deletions src/main/kotlin/net/ccbluex/netty/http/util/Base64.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 22 additions & 4 deletions src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,22 +78,26 @@ 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)
)

/**
* 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.
* @param gson The Gson instance to serialize the body.
* @return A FullHttpResponse object.
*/
fun <T : Any> httpResponse(status: HttpResponseStatus, json: T) = httpResponse(
@JvmOverloads
fun <T : Any> httpResponse(status: HttpResponseStatus, json: T, gson: Gson = DEFAULT_GSON) = httpResponse(
status,
"application/json",
PooledByteBufAllocator.DEFAULT.writeJson(json, json.javaClass)
Expand All @@ -104,7 +109,20 @@ fun <T : Any> 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 <T : Any> 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.
Expand Down
15 changes: 8 additions & 7 deletions src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -48,11 +49,11 @@ fun ByteBufAllocator.writeJson(
fun <T : Any> 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
}
94 changes: 94 additions & 0 deletions src/main/kotlin/net/ccbluex/netty/http/util/TransportDetector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
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 {
private val available by lazy {
arrayOf(Epoll, KQueue, Nio).first { it.isAvailable }
}

/**
* 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<EventLoopGroup, EventLoopGroup> {
val parentGroup = available.newParentGroup()
val childGroup = available.newChildGroup()
bootstrap.group(parentGroup, childGroup)
.channelFactory(available::newServerChannel)
return parentGroup to childGroup
}
}
}
Loading