HelidonServerAdapter.kt
package com.hexagonkt.http.server.helidon
import com.hexagonkt.core.fieldsMapOf
import com.hexagonkt.core.security.createKeyManagerFactory
import com.hexagonkt.core.security.createTrustManagerFactory
import com.hexagonkt.core.toText
import com.hexagonkt.http.SslSettings
import com.hexagonkt.http.handlers.bodyToBytes
import com.hexagonkt.http.handlers.HttpHandler
import com.hexagonkt.http.model.HttpProtocol
import com.hexagonkt.http.model.HttpProtocol.*
import com.hexagonkt.http.model.HttpResponse
import com.hexagonkt.http.model.HttpResponsePort
import com.hexagonkt.http.server.HttpServer
import com.hexagonkt.http.server.HttpServerFeature
import com.hexagonkt.http.server.HttpServerFeature.ZIP
import com.hexagonkt.http.server.HttpServerPort
import io.helidon.common.socket.SocketOptions
import io.helidon.http.Method
import io.helidon.http.Status
import io.helidon.http.HeaderNames
import io.helidon.http.HttpMediaType
import io.helidon.http.SetCookie
import io.helidon.webserver.WebServer
import io.helidon.webserver.http.ServerResponse
import io.helidon.webserver.http1.Http1Config
import io.helidon.webserver.http2.Http2Config
import java.security.SecureRandom
import java.time.Duration
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLParameters
import javax.net.ssl.TrustManagerFactory
/**
* Implements [HttpServerPort] using Helidon.
*
* TODO Add settings for HTTP2 and separate them on constructor parameters
*/
class HelidonServerAdapter(
private val backlog: Int = 1_024,
private val writeQueueLength: Int = 0,
private val readTimeout: Duration = Duration.ofSeconds(30),
private val connectTimeout: Duration = Duration.ofSeconds(10),
private val tcpNoDelay: Boolean = false,
private val receiveLog: Boolean = true,
private val sendLog: Boolean = true,
private val validatePath: Boolean = true,
private val validateRequestHeaders: Boolean = true,
private val validateResponseHeaders: Boolean = false,
) : HttpServerPort {
private companion object {
const val START_ERROR_MESSAGE = "Helidon server not started correctly"
}
private var helidonServer: WebServer? = null
override fun runtimePort(): Int {
return helidonServer?.port() ?: error(START_ERROR_MESSAGE)
}
override fun started() =
helidonServer?.isRunning ?: false
override fun startUp(server: HttpServer) {
val settings = server.settings
val sslSettings = settings.sslSettings
val handlers: Map<Method, HttpHandler> =
server.handler.addPrefix(settings.contextPath)
.byMethod()
.mapKeys { Method.create(it.key.toString()) }
// TODO features(): [Config, Encoding, Media, WebServer] Maybe Multipart can be added there
val serverBuilder = WebServer
.builder()
.host(settings.bindAddress.hostName)
.port(settings.bindPort)
.routing {
it.any({ helidonRequest, helidonResponse ->
val method = helidonRequest.prologue().method()
val request = HelidonRequestAdapter(method, helidonRequest)
val response = handlers[method]?.process(request)?.response ?: HttpResponse()
setResponse(request.protocol.secure, response, helidonResponse)
})
}
if (sslSettings != null)
serverBuilder.tls {
val sslClientAuth = sslSettings.clientAuth
it
.sslParameters(SSLParameters().apply { needClientAuth = sslClientAuth })
.sslContext(sslContext(sslSettings))
}
val protocolConfig =
if (settings.protocol == HTTP || settings.protocol == HTTPS)
Http1Config
.builder()
.receiveLog(receiveLog)
.sendLog(sendLog)
.validatePath(validatePath)
.validateRequestHeaders(validateRequestHeaders)
.validateResponseHeaders(validateResponseHeaders)
.build()
else
Http2Config
.builder()
.validatePath(validatePath)
.build()
helidonServer = serverBuilder
.backlog(backlog)
.writeQueueLength(writeQueueLength)
.connectionOptions(SocketOptions
.builder()
.readTimeout(readTimeout)
.connectTimeout(connectTimeout)
.tcpNoDelay(tcpNoDelay)
.build()
)
.protocols(listOf(protocolConfig))
.build()
helidonServer?.start() ?: error(START_ERROR_MESSAGE)
}
override fun shutDown() {
helidonServer?.stop() ?: error(START_ERROR_MESSAGE)
}
override fun supportedProtocols(): Set<HttpProtocol> =
setOf(HTTP, HTTPS, HTTP2)
override fun supportedFeatures(): Set<HttpServerFeature> =
setOf(ZIP)
override fun options(): Map<String, *> =
fieldsMapOf(
HelidonServerAdapter::backlog to backlog,
HelidonServerAdapter::writeQueueLength to writeQueueLength,
HelidonServerAdapter::readTimeout to readTimeout,
HelidonServerAdapter::connectTimeout to connectTimeout,
HelidonServerAdapter::tcpNoDelay to tcpNoDelay,
HelidonServerAdapter::receiveLog to receiveLog,
HelidonServerAdapter::sendLog to sendLog,
HelidonServerAdapter::validatePath to validatePath,
HelidonServerAdapter::validateRequestHeaders to validateRequestHeaders,
HelidonServerAdapter::validateResponseHeaders to validateResponseHeaders,
)
private fun setResponse(
secureRequest: Boolean,
response: HttpResponsePort,
helidonResponse: ServerResponse
) {
try {
helidonResponse.status(Status.create(response.status.code))
response.headers.values.forEach {
helidonResponse.header(HeaderNames.create(it.name), *it.strings().toTypedArray())
}
val headers = helidonResponse.headers()
response.cookies
.filter { if (secureRequest) true else !it.secure }
.forEach {
val cookie = SetCookie
.builder(it.name, it.value)
.maxAge(Duration.ofSeconds(it.maxAge))
.path(it.path)
.httpOnly(it.httpOnly)
.secure(it.secure)
if (it.expires != null)
cookie.expires(it.expires)
if (it.deleted)
headers.clearCookie(it.name)
else
headers.addCookie(cookie.build())
}
response.contentType?.let { ct -> headers.contentType(HttpMediaType.create(ct.text)) }
helidonResponse.send(bodyToBytes(response.body))
}
catch (e: Exception) {
helidonResponse.status(Status.INTERNAL_SERVER_ERROR_500)
helidonResponse.send(bodyToBytes(e.toText()))
}
}
private fun sslContext(sslSettings: SslSettings): SSLContext {
val keyManager = keyManagerFactory(sslSettings)
val trustManager = trustManagerFactory(sslSettings)
val eng = SSLContext.getDefault().createSSLEngine()
eng.needClientAuth = sslSettings.clientAuth
val context = SSLContext.getInstance("TLSv1.3")
context.init(
keyManager.keyManagers,
trustManager?.trustManagers ?: emptyArray(),
SecureRandom.getInstanceStrong()
)
return context
}
private fun trustManagerFactory(sslSettings: SslSettings): TrustManagerFactory? {
val trustStoreUrl = sslSettings.trustStore ?: return null
val trustStorePassword = sslSettings.trustStorePassword
return createTrustManagerFactory(trustStoreUrl, trustStorePassword)
}
private fun keyManagerFactory(sslSettings: SslSettings): KeyManagerFactory {
val keyStoreUrl = sslSettings.keyStore ?: error("")
val keyStorePassword = sslSettings.keyStorePassword
return createKeyManagerFactory(keyStoreUrl, keyStorePassword)
}
}