HelidonHttpServer.kt
package com.hexagontk.http.server.helidon
import com.hexagontk.core.fieldsMapOf
import com.hexagontk.core.media.TEXT_PLAIN
import com.hexagontk.core.security.createKeyManagerFactory
import com.hexagontk.core.security.createTrustManagerFactory
import com.hexagontk.core.toText
import com.hexagontk.http.SslSettings
import com.hexagontk.http.handlers.bodyToBytes
import com.hexagontk.http.handlers.HttpHandler
import com.hexagontk.http.model.HttpProtocol
import com.hexagontk.http.model.HttpProtocol.*
import com.hexagontk.http.model.HttpResponse
import com.hexagontk.http.model.HttpResponsePort
import com.hexagontk.http.server.HttpServer
import com.hexagontk.http.HttpFeature
import com.hexagontk.http.HttpFeature.*
import com.hexagontk.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.HeaderNames.CONTENT_TYPE
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 HelidonHttpServer(
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
constructor() : this(
backlog = 1_024,
writeQueueLength = 0,
readTimeout = Duration.ofSeconds(30),
connectTimeout = Duration.ofSeconds(10),
tcpNoDelay = false,
receiveLog = true,
sendLog = true,
validatePath = true,
validateRequestHeaders = true,
validateResponseHeaders = false
)
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
.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<HttpFeature> =
setOf(ZIP, COOKIES, MULTIPART)
override fun options(): Map<String, *> =
fieldsMapOf(
HelidonHttpServer::backlog to backlog,
HelidonHttpServer::writeQueueLength to writeQueueLength,
HelidonHttpServer::readTimeout to readTimeout,
HelidonHttpServer::connectTimeout to connectTimeout,
HelidonHttpServer::tcpNoDelay to tcpNoDelay,
HelidonHttpServer::receiveLog to receiveLog,
HelidonHttpServer::sendLog to sendLog,
HelidonHttpServer::validatePath to validatePath,
HelidonHttpServer::validateRequestHeaders to validateRequestHeaders,
HelidonHttpServer::validateResponseHeaders to validateResponseHeaders,
)
private fun setResponse(
secureRequest: Boolean,
response: HttpResponsePort,
helidonResponse: ServerResponse
) {
try {
helidonResponse.status(Status.create(response.status))
response.headers.all.forEach { (k, v) ->
helidonResponse.header(HeaderNames.create(k), *v.map { it.text }.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.header(CONTENT_TYPE, TEXT_PLAIN.fullType)
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)
}
}