JdkHttpServer.kt
- package com.hexagontk.http.server.jdk
- 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.model.HttpProtocol
- import com.hexagontk.http.model.HttpProtocol.*
- import com.hexagontk.http.server.HttpServer
- import com.hexagontk.http.HttpFeature
- import com.hexagontk.http.handlers.bodyToBytes
- import com.hexagontk.http.model.HttpResponse
- import com.hexagontk.http.model.HttpResponsePort
- import com.hexagontk.http.model.INTERNAL_SERVER_ERROR_500
- import com.hexagontk.http.server.HttpServerPort
- import com.sun.net.httpserver.Headers
- import com.sun.net.httpserver.HttpExchange
- import com.sun.net.httpserver.HttpsConfigurator
- import com.sun.net.httpserver.HttpServer as SunHttpServer
- import com.sun.net.httpserver.HttpsServer as SunHttpsServer
- import java.net.InetSocketAddress
- import com.sun.net.httpserver.HttpHandler as SunHttpHandler
- import java.security.SecureRandom
- import java.util.concurrent.Executor
- import javax.net.ssl.KeyManagerFactory
- import javax.net.ssl.SSLContext
- import javax.net.ssl.TrustManagerFactory
- /**
- * Implements [HttpServerPort] using the JDK HTTP server.
- *
- * @param backlog .
- * @param executor .
- * @param stopDelay .
- * @param idleInterval Maximum duration in seconds which an idle connection is kept open. This timer
- * has an implementation specific granularity that may mean that idle connections are closed later
- * than the specified interval. Values less than or equal to zero are mapped to* the default
- * setting.
- * @param maxConnections The maximum number of open connections at a time. This includes active
- * and idle connections. If zero or negative, then no limit is enforced.
- * @param maxIdleConnections The maximum number of idle connections at a time. If set to zero or a
- * negative value then connections are closed after use.
- * @param drainAmount The maximum number of bytes that will be automatically read and discarded
- * from a request body that has not been completely consumed by its HttpHandler. If the number of
- * remaining unread bytes are less than this limit then the connection will be put in the idle
- * connection cache. If not, then it will be closed.
- * @param maxReqHeaders The maximum number of header fields accepted in a request. If this limit is
- * exceeded while the headers are being read, then the connection is terminated and the request
- * ignored. If the value is less than or equal to zero, then the default value is used.
- * @param maxReqTime The maximum time in milliseconds allowed to receive a request headers and body.
- * In practice, the actual time is a function of request size, network speed, and handler
- * processing delays. A value less than or equal to zero means the time is not limited. If the
- * limit is exceeded then the connection is terminated and the handler will receive a IOException.
- * This timer has an implementation specific granularity that may mean requests are aborted later
- * than the specified interval.
- * @param maxRspTime The maximum time in milliseconds allowed to receive a response headers and
- * body. In practice, the actual time is a function of response size, network speed, and handler
- * processing delays. A value less than or equal to zero means the time is not limited. If the
- * limit is exceeded then the connection is terminated and the handler will receive a IOException.
- * This timer has an implementation specific granularity that may mean responses are aborted later
- * than the specified interval.
- * @param nodelay If true, sets the TCP_NODELAY socket option on all incoming connections.
- */
- class JdkHttpServer(
- private val backlog: Int = 1_024,
- private val executor: Executor? = null,
- private val stopDelay: Int = 0,
- private val idleInterval: Int = 30,
- private val maxConnections: Int = -1,
- private val maxIdleConnections: Int = 200,
- private val drainAmount: Int = 65536,
- private val maxReqHeaders: Int = 200,
- private val maxReqTime: Int = -1,
- private val maxRspTime: Int = -1,
- private val nodelay: Boolean = false,
- ) : HttpServerPort {
- private companion object {
- const val START_ERROR_MESSAGE = "JDK HTTP server not started correctly"
- }
- private var started = false
- private var sunServer: SunHttpServer? = null
- constructor() : this(
- backlog = 1_024,
- executor = null,
- stopDelay = 0,
- idleInterval = 30,
- maxConnections = -1,
- maxIdleConnections = 200,
- drainAmount = 65536,
- maxReqHeaders = 200,
- maxReqTime = -1,
- maxRspTime = -1,
- nodelay = false,
- )
- init {
- System.setProperty("sun.net.httpserver.idleInterval", idleInterval.toString())
- System.setProperty("jdk.httpserver.maxConnections", maxConnections.toString())
- System.setProperty("sun.net.httpserver.maxIdleConnections", maxIdleConnections.toString())
- System.setProperty("sun.net.httpserver.drainAmount", drainAmount.toString())
- System.setProperty("sun.net.httpserver.maxReqHeaders", maxReqHeaders.toString())
- System.setProperty("sun.net.httpserver.maxReqTime", maxReqTime.toString())
- System.setProperty("sun.net.httpserver.maxRspTime", maxRspTime.toString())
- System.setProperty("sun.net.httpserver.nodelay", nodelay.toString())
- }
- override fun runtimePort(): Int {
- return sunServer?.address?.port ?: error(START_ERROR_MESSAGE)
- }
- override fun started() =
- started
- override fun startUp(server: HttpServer) {
- val settings = server.settings
- val sslSettings = settings.sslSettings
- val handlers = server.handler.byMethod().mapKeys { it.key.toString() }
- val host = settings.bindAddress.hostName
- val port = settings.bindPort
- val address = InetSocketAddress(host, port)
- var jdkServer =
- if (sslSettings == null)
- SunHttpServer.create(address, backlog)
- else
- SunHttpsServer.create(address, backlog).apply {
- val sslContext = sslContext(sslSettings)
- httpsConfigurator = HttpsConfigurator(sslContext)
- }
- jdkServer.createContext("/", object : SunHttpHandler {
- override fun handle(exchange: HttpExchange) {
- val method = exchange.requestMethod
- val request = JdkRequestAdapter(method, exchange)
- val response = handlers[method]?.process(request)?.response ?: HttpResponse()
- reply(response, exchange)
- }
- })
- jdkServer.executor = executor
- jdkServer.start()
- sunServer = jdkServer
- started = true
- }
- override fun shutDown() {
- sunServer?.stop(stopDelay) ?: error(START_ERROR_MESSAGE)
- started = false
- }
- override fun supportedProtocols(): Set<HttpProtocol> =
- setOf(HTTP, HTTPS)
- override fun supportedFeatures(): Set<HttpFeature> =
- emptySet()
- override fun options(): Map<String, *> =
- fieldsMapOf(
- JdkHttpServer::backlog to backlog,
- JdkHttpServer::executor to executor,
- JdkHttpServer::stopDelay to stopDelay,
- JdkHttpServer::idleInterval to idleInterval,
- JdkHttpServer::maxConnections to maxConnections,
- JdkHttpServer::maxIdleConnections to maxIdleConnections,
- JdkHttpServer::drainAmount to drainAmount,
- JdkHttpServer::maxReqHeaders to maxReqHeaders,
- JdkHttpServer::maxReqTime to maxReqTime,
- JdkHttpServer::maxRspTime to maxRspTime,
- JdkHttpServer::nodelay to nodelay,
- )
- private fun reply(response: HttpResponsePort, exchange: HttpExchange) {
- var headers = exchange.responseHeaders
- try {
- response.headers.forEach { headers.add(it.name, it.text) }
- send(response.status, response.body, response.contentType?.text, headers, exchange)
- }
- catch (e: Exception) {
- send(INTERNAL_SERVER_ERROR_500, e.toText(), TEXT_PLAIN.fullType, headers, exchange)
- }
- finally {
- exchange.close()
- }
- }
- private fun send(
- status: Int, result: Any, contentType: String?, headers: Headers, exchange: HttpExchange
- ) {
- if (contentType != null)
- headers.set("content-type", contentType)
- var body = bodyToBytes(result)
- val size = if (body.isEmpty()) -1 else body.size.toLong()
- exchange.sendResponseHeaders(status, size)
- if (size != -1L)
- exchange.responseBody.use { it.write(body) }
- }
- 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)
- }
- }