-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Introduces support for HTTP Response middleware - Generic CORS middleware - Adds various useful response utils
- Loading branch information
Showing
8 changed files
with
291 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
src/main/kotlin/net/ccbluex/netty/http/middleware/CorsMiddleware.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package net.ccbluex.netty.http.middleware | ||
|
||
import io.netty.handler.codec.http.FullHttpResponse | ||
import io.netty.handler.codec.http.HttpHeaderNames | ||
import net.ccbluex.netty.http.HttpServer.Companion.logger | ||
import net.ccbluex.netty.http.model.RequestContext | ||
import java.net.URI | ||
import java.net.URISyntaxException | ||
|
||
/** | ||
* Middleware to handle Cross-Origin Resource Sharing (CORS) requests. | ||
* | ||
* @param allowedOrigins List of allowed (host) origins (default: localhost, 127.0.0.1) | ||
* - If we want to specify a protocol and port, we should use the full origin (e.g., http://localhost:8080). | ||
* @param allowedMethods List of allowed HTTP methods (default: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) | ||
* @param allowedHeaders List of allowed HTTP headers (default: Content-Type, Content-Length, Authorization, Accept, X-Requested-With) | ||
* | ||
* @see RequestContext | ||
*/ | ||
class CorsMiddleware( | ||
private val allowedOrigins: List<String> = | ||
listOf("localhost", "127.0.0.1"), | ||
private val allowedMethods: List<String> = | ||
listOf("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"), | ||
private val allowedHeaders: List<String> = | ||
listOf("Content-Type", "Content-Length", "Authorization", "Accept", "X-Requested-With") | ||
): Middleware { | ||
|
||
/** | ||
* Middleware to handle CORS requests. | ||
* Pass to server.middleware() to apply the CORS policy to all requests. | ||
*/ | ||
override fun middleware(context: RequestContext, response: FullHttpResponse): FullHttpResponse { | ||
val httpHeaders = response.headers() | ||
val requestOrigin = context.headers["origin"] ?: context.headers["Origin"] | ||
|
||
if (requestOrigin != null) { | ||
try { | ||
// Parse the origin to extract the hostname (ignoring the port) | ||
val uri = URI(requestOrigin) | ||
val host = uri.host | ||
|
||
// Allow requests from localhost or 127.0.0.1 regardless of the port | ||
if (allowedOrigins.contains(host) || allowedOrigins.contains(requestOrigin)) { | ||
httpHeaders[HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN] = requestOrigin | ||
} else { | ||
// Block cross-origin requests by not allowing other origins | ||
httpHeaders[HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN] = "null" | ||
} | ||
} catch (e: URISyntaxException) { | ||
// Handle bad URIs by setting a default CORS policy or logging the error | ||
httpHeaders[HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN] = "null" | ||
logger.error("Invalid Origin header: $requestOrigin", e) | ||
} | ||
|
||
// Allow specific methods and headers for cross-origin requests | ||
httpHeaders[HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS] = allowedMethods.joinToString(", ") | ||
httpHeaders[HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS] = allowedHeaders.joinToString(", ") | ||
} | ||
|
||
return response | ||
} | ||
|
||
} |
10 changes: 10 additions & 0 deletions
10
src/main/kotlin/net/ccbluex/netty/http/middleware/Middleware.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package net.ccbluex.netty.http.middleware | ||
|
||
import io.netty.handler.codec.http.FullHttpResponse | ||
import net.ccbluex.netty.http.model.RequestContext | ||
|
||
typealias MiddlewareFunction = (RequestContext, FullHttpResponse) -> FullHttpResponse | ||
|
||
interface Middleware { | ||
fun middleware(context: RequestContext, response: FullHttpResponse): FullHttpResponse | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import com.google.gson.JsonObject | ||
import io.netty.handler.codec.http.FullHttpResponse | ||
import net.ccbluex.netty.http.HttpServer | ||
import net.ccbluex.netty.http.model.RequestObject | ||
import net.ccbluex.netty.http.util.httpOk | ||
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 | ||
import kotlin.test.assertTrue | ||
|
||
/** | ||
* Test class for the HttpServer, focusing on verifying the routing capabilities | ||
* and correctness of responses from different endpoints. | ||
*/ | ||
@TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
class HttpMiddlewareServerTest { | ||
|
||
private lateinit var serverThread: Thread | ||
private val client = OkHttpClient() | ||
|
||
/** | ||
* This method sets up the necessary environment before any tests are run. | ||
* It creates a temporary directory with dummy files and starts the HTTP server | ||
* in a separate thread. | ||
*/ | ||
@BeforeAll | ||
fun initialize() { | ||
// Start the HTTP server in a separate thread | ||
serverThread = thread { | ||
startHttpServer() | ||
} | ||
|
||
// Allow the server some time to start | ||
Thread.sleep(1000) | ||
} | ||
|
||
/** | ||
* This method cleans up resources after all tests have been executed. | ||
* It stops the server and deletes the temporary directory. | ||
*/ | ||
@AfterAll | ||
fun cleanup() { | ||
serverThread.interrupt() | ||
} | ||
|
||
/** | ||
* This function starts the HTTP server with routing configured for | ||
* different difficulty levels. | ||
*/ | ||
private fun startHttpServer() { | ||
val server = HttpServer() | ||
|
||
server.routeController.apply { | ||
get("/", ::static) | ||
} | ||
|
||
server.middleware { requestContext, fullHttpResponse -> | ||
// Add custom headers to the response | ||
fullHttpResponse.headers().add("X-Custom-Header", "Custom Value") | ||
|
||
// Add a custom header if there is a query parameter | ||
if (requestContext.params.isNotEmpty()) { | ||
fullHttpResponse.headers().add("X-Query-Param", | ||
requestContext.params.entries.joinToString(",")) | ||
} | ||
|
||
fullHttpResponse | ||
} | ||
|
||
server.start(8080) // Start the server on port 8080 | ||
} | ||
|
||
@Suppress("UNUSED_PARAMETER") | ||
fun static(requestObject: RequestObject): FullHttpResponse { | ||
return httpOk(JsonObject().apply { | ||
addProperty("message", "Hello, World!") | ||
}) | ||
} | ||
|
||
/** | ||
* Utility function to make HTTP GET requests to the specified path. | ||
* | ||
* @param path The path for the request. | ||
* @return The HTTP response. | ||
*/ | ||
private fun makeRequest(path: String): Response { | ||
val request = Request.Builder() | ||
.url("http://localhost:8080$path") | ||
.build() | ||
return client.newCall(request).execute() | ||
} | ||
|
||
/** | ||
* Test the root endpoint ("/") and verify that it returns the correct number | ||
* of files in the directory. | ||
*/ | ||
@Test | ||
fun testRootEndpoint() { | ||
val response = makeRequest("/") | ||
assertEquals(200, response.code(), "Expected status code 200") | ||
|
||
val responseBody = response.body()?.string() | ||
assertNotNull(responseBody, "Response body should not be null") | ||
|
||
assertTrue(responseBody.contains("Hello, World!"), "Response should contain 'Hello, World!'") | ||
} | ||
|
||
/** | ||
* Test the root endpoint ("/") with a query parameter and verify that the | ||
* custom header is added to the response. | ||
*/ | ||
@Test | ||
fun testRootEndpointWithQueryParam() { | ||
val response = makeRequest("/?param1=value1¶m2=value2") | ||
assertEquals(200, response.code(), "Expected status code 200") | ||
|
||
val responseBody = response.body()?.string() | ||
assertNotNull(responseBody, "Response body should not be null") | ||
|
||
assertTrue(responseBody.contains("Hello, World!"), "Response should contain 'Hello, World!'") | ||
assertTrue(response.headers("X-Custom-Header").contains("Custom Value"), "Custom header should be present") | ||
assertTrue(response.headers("X-Query-Param").contains("param1=value1,param2=value2"), | ||
"Query parameter should be present in the response") | ||
} | ||
|
||
} |