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
19 changes: 5 additions & 14 deletions src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
*/
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
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

internal class HttpConductor(private val server: HttpServer) {

Expand All @@ -48,22 +48,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.wrappedBuffer(ByteArray(0))
)

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,
Expand Down
7 changes: 3 additions & 4 deletions src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
)

Expand All @@ -95,21 +95,20 @@ 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))

// 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) }
Expand Down
35 changes: 15 additions & 20 deletions src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,32 @@
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<String, String>) {
val contentBuffer = StringBuilder()
val path = if (uri.contains("?")) uri.substring(0, uri.indexOf('?')) else uri
val path = uri.substringBefore('?', uri)
val params = getUriParams(uri)
}

/**
* The received uri should be like: '...?param1=value&param2=value'
*/
private fun getUriParams(uri: String): Map<String, String> {
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<String> -> paramValue[0] },
{ paramValue: Array<String> -> 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()
}
9 changes: 7 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 @@ -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.
Expand Down Expand Up @@ -51,7 +51,12 @@ data class RequestObject(
* @return The JSON object of the specified type.
*/
inline fun <reified T> asJson(): T {
return Gson().fromJson(body, T::class.java)
return GSON_INSTANCE.fromJson(body, T::class.java)
}

companion object {
@JvmField
val GSON_INSTANCE = gson
}

}
8 changes: 6 additions & 2 deletions src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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()
private fun String.asPathArray(): Array<String> {
val parts = split("/")
return if (parts.size <= 1) emptyArray()
else parts.subList(1, parts.size).toTypedArray()
}
29 changes: 29 additions & 0 deletions src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
*/
@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)
Loading