Skip to content

Commit

Permalink
Develop Netty HTTP client
Browse files Browse the repository at this point in the history
  • Loading branch information
jaguililla committed May 16, 2024
1 parent 3b0bbf9 commit d9b2f0d
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 223 deletions.
4 changes: 4 additions & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ public final class com/hexagonkt/core/security/CryptoKt {
}

public final class com/hexagonkt/core/security/KeyStoresKt {
public static final fun createKeyManagerFactory (Ljava/net/URL;Ljava/lang/String;Ljava/lang/String;)Ljavax/net/ssl/KeyManagerFactory;
public static synthetic fun createKeyManagerFactory$default (Ljava/net/URL;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljavax/net/ssl/KeyManagerFactory;
public static final fun createTrustManagerFactory (Ljava/net/URL;Ljava/lang/String;Ljava/lang/String;)Ljavax/net/ssl/TrustManagerFactory;
public static synthetic fun createTrustManagerFactory$default (Ljava/net/URL;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljavax/net/ssl/TrustManagerFactory;
public static final fun getPrivateKey (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Ljava/security/interfaces/RSAPrivateKey;
public static final fun getPublicKey (Ljava/security/KeyStore;Ljava/lang/String;)Ljava/security/interfaces/RSAPublicKey;
public static final fun loadKeyStore (Ljava/net/URL;Ljava/lang/String;)Ljava/security/KeyStore;
Expand Down
24 changes: 24 additions & 0 deletions core/src/main/kotlin/com/hexagonkt/core/security/KeyStores.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import java.net.URL
import java.security.KeyStore
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory

// TODO Create CAs and PKs like `certificates.gradle` Check: https://www.baeldung.com/java-keystore
fun loadKeyStore(resource: URL, password: String): KeyStore =
Expand All @@ -16,3 +18,25 @@ fun KeyStore.getPrivateKey(alias: String, password: String): RSAPrivateKey =

fun KeyStore.getPublicKey(alias: String): RSAPublicKey =
this.getCertificate(alias).publicKey as RSAPublicKey

fun createTrustManagerFactory(
resource: URL,
password: String,
algorithm: String = TrustManagerFactory.getDefaultAlgorithm()
): TrustManagerFactory {
val trustStore = loadKeyStore(resource, password)
val trustManager = TrustManagerFactory.getInstance(algorithm)
trustManager.init(trustStore)
return trustManager
}

fun createKeyManagerFactory(
resource: URL,
password: String,
algorithm: String = KeyManagerFactory.getDefaultAlgorithm()
): KeyManagerFactory {
val keyStore = loadKeyStore(resource, password)
val keyManager = KeyManagerFactory.getInstance(algorithm)
keyManager.init(keyStore, password.toCharArray())
return keyManager
}
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ junitVersion=5.10.2
gatlingVersion=3.10.5
slf4jVersion=2.0.13
jmhVersion=1.37
mkdocsMaterialVersion=9.5.21
mkdocsMaterialVersion=9.5.23
mermaidDokkaVersion=0.6.0
nativeToolsVersion=0.10.1

Expand Down Expand Up @@ -71,7 +71,7 @@ invokerVersion=1.3.1
freemarkerVersion=2.3.32

# templates_jte
jteVersion=3.1.10
jteVersion=3.1.11

# templates_pebble
pebbleVersion=3.2.2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hexagonkt.http.client.netty

import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelPromise
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http2.Http2Settings
import java.util.concurrent.TimeUnit

internal class Http2SettingsHandler(private val promise: ChannelPromise) :
SimpleChannelInboundHandler<Http2Settings>() {
fun awaitSettings(timeout: Long, unit: TimeUnit?) {
check(promise.awaitUninterruptibly(timeout, unit)) { "Timed out waiting for settings" }
}

override fun channelRead0(ctx: ChannelHandlerContext, msg: Http2Settings) {
promise.setSuccess()

ctx.pipeline().remove(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class HttpChannelInitializer(
override fun initChannel(channel: SocketChannel) {
val pipeline = channel.pipeline()

pipeline.addLast(HttpServerCodec())
pipeline.addLast(HttpClientCodec())

if (keepAliveHandler)
pipeline.addLast(HttpServerKeepAliveHandler())
Expand All @@ -25,7 +25,7 @@ internal class HttpChannelInitializer(
if (chunkedHandler)
pipeline.addLast(ChunkedWriteHandler())

val nettyServerHandler = Http2ClientResponseHandler()
val nettyServerHandler = HttpClientResponseHandler()

if (executorGroup == null)
pipeline.addLast(nettyServerHandler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.hexagonkt.http.client.netty

import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.LastHttpContent
import io.netty.util.CharsetUtil

class HttpClientResponseHandler : SimpleChannelInboundHandler<FullHttpResponse>() {

lateinit var response: FullHttpResponse
override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpResponse) {
System.err.println("STATUS: " + msg.status())
System.err.println("VERSION: " + msg.protocolVersion())
System.err.println()
response = msg

if (!msg.headers().isEmpty) {
for (name in msg.headers().names()) {
for (value in msg.headers().getAll(name)) {
System.err.println("HEADER: $name = $value")
}
}
System.err.println()
}

if (HttpUtil.isTransferEncodingChunked(msg)) {
System.err.println("CHUNKED CONTENT {")
} else {
System.err.println("CONTENT {")
}

System.err.print(msg.content().toString(CharsetUtil.UTF_8))
System.err.flush()

if (msg is LastHttpContent) {
System.err.println("} END OF CONTENT")
ctx.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package com.hexagonkt.http.client.netty

import com.hexagonkt.http.SslSettings
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.socket.SocketChannel
import io.netty.handler.codec.http.*
import io.netty.handler.codec.http2.DefaultHttp2Connection
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener
import io.netty.handler.codec.http2.Http2Connection
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder
import io.netty.handler.ssl.ApplicationProtocolNames
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler
import io.netty.handler.ssl.SslContext
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.EventExecutorGroup

internal class HttpsChannelInitializer(
private val sslContext: SslContext,
private val sslSettings: SslSettings,
private val executorGroup: EventExecutorGroup?,
private val executorGroup: EventExecutorGroup? = null,
private val keepAliveHandler: Boolean = true,
private val httpAggregatorHandler: Boolean = true,
private val chunkedHandler: Boolean = true,
private val maxContentLength: Int = Int.MAX_VALUE,
val responseHandler: Http2ClientResponseHandler = Http2ClientResponseHandler(),
) : ChannelInitializer<SocketChannel>() {

lateinit var settingsHandler: Http2SettingsHandler

override fun initChannel(channel: SocketChannel) {
settingsHandler = Http2SettingsHandler(channel.newPromise())

val pipeline = channel.pipeline()
val sslHandler = sslContext.newHandler(channel.alloc())
val handlerSsl = if (sslSettings.clientAuth) sslHandler else null

pipeline.addLast(sslHandler)
pipeline.addLast(HttpServerCodec())
Expand All @@ -32,11 +43,48 @@ internal class HttpsChannelInitializer(
if (chunkedHandler)
pipeline.addLast(ChunkedWriteHandler())

val serverHandler = Http2ClientResponseHandler()
val responseHandler = Http2ClientResponseHandler()
val c = getClientAPNHandler(maxContentLength, settingsHandler, responseHandler)

if (executorGroup == null)
pipeline.addLast(serverHandler)
pipeline.addLast(c)
else
pipeline.addLast(executorGroup, serverHandler)
pipeline.addLast(executorGroup, c)
}

private fun getClientAPNHandler(
maxContentLength: Int,
settingsHandler: Http2SettingsHandler?,
responseHandler: Http2ClientResponseHandler?
): ApplicationProtocolNegotiationHandler {
val connection: Http2Connection = DefaultHttp2Connection(false)

val connectionHandler = HttpToHttp2ConnectionHandlerBuilder()
.frameListener(
DelegatingDecompressorFrameListener(
connection,
InboundHttp2ToHttpAdapterBuilder(connection).maxContentLength(maxContentLength)
.propagateSettings(true)
.build()
)
)
.connection(connection)
.build()

val clientAPNHandler: ApplicationProtocolNegotiationHandler =
object : ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
override fun configurePipeline(ctx: ChannelHandlerContext, protocol: String) {
if (ApplicationProtocolNames.HTTP_2 == protocol) {
val p = ctx.pipeline()
p.addLast(connectionHandler)
p.addLast(settingsHandler, responseHandler)
return
}
ctx.close()
throw IllegalStateException("Protocol: $protocol not supported")
}
}

return clientAPNHandler
}
}
Loading

0 comments on commit d9b2f0d

Please sign in to comment.