JdkHttpClient.kt

  1. package com.hexagontk.http.client.jdk

  2. import com.hexagontk.core.security.createKeyManagerFactory
  3. import com.hexagontk.core.security.createTrustManagerFactory
  4. import com.hexagontk.http.*
  5. import com.hexagontk.http.HttpFeature.*
  6. import com.hexagontk.http.client.HttpClient
  7. import com.hexagontk.http.client.HttpClientPort
  8. import com.hexagontk.http.client.HttpClientSettings
  9. import com.hexagontk.http.handlers.bodyToBytes
  10. import com.hexagontk.http.model.*
  11. import com.hexagontk.http.model.HttpProtocol.*
  12. import com.hexagontk.http.model.HttpResponse
  13. import com.hexagontk.http.model.ws.WsSession
  14. import java.net.CookieManager
  15. import java.net.HttpCookie
  16. import java.net.URI
  17. import java.net.http.HttpClient.Redirect.ALWAYS
  18. import java.net.http.HttpClient.Redirect.NEVER
  19. import java.net.http.HttpClient.Version.HTTP_1_1
  20. import java.net.http.HttpClient.Version.HTTP_2
  21. import java.net.http.HttpHeaders
  22. import java.net.http.HttpRequest.BodyPublishers
  23. import java.net.http.HttpResponse.BodyHandlers
  24. import java.security.SecureRandom
  25. import java.security.cert.X509Certificate
  26. import java.util.concurrent.Executor
  27. import java.util.concurrent.Flow.Publisher
  28. import javax.net.ssl.KeyManagerFactory
  29. import javax.net.ssl.SSLContext
  30. import javax.net.ssl.TrustManagerFactory
  31. import javax.net.ssl.X509TrustManager
  32. import java.net.http.HttpClient as JavaClient
  33. import java.net.http.HttpRequest as JavaHttpRequest
  34. import java.net.http.HttpResponse as JavaHttpResponse

  35. /**
  36.  * Client to use other REST services.
  37.  */
  38. class JdkHttpClient(
  39.     private val protocol: HttpProtocol = HTTP2,
  40.     private val executor: Executor? = null,
  41. ) : HttpClientPort {

  42.     private companion object {
  43.         object TrustAll : X509TrustManager {
  44.             override fun getAcceptedIssuers(): Array<X509Certificate> =
  45.                 emptyArray()

  46.             override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}

  47.             override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
  48.         }
  49.     }

  50.     private lateinit var javaClient: JavaClient
  51.     private lateinit var httpClient: HttpClient
  52.     private lateinit var httpSettings: HttpClientSettings
  53.     private var started: Boolean = false
  54.     private val cookieManager: CookieManager by lazy { CookieManager() }

  55.     constructor() : this(HTTP2, null)

  56.     override fun startUp(client: HttpClient) {
  57.         val settings = client.settings

  58.         httpClient = client
  59.         httpSettings = settings
  60.         val javaClientBuilder = JavaClient
  61.             .newBuilder()
  62.             .version(if (protocol == HTTP2 || protocol == H2C) HTTP_2 else HTTP_1_1)
  63.             .followRedirects(if (settings.followRedirects) ALWAYS else NEVER)

  64.         if (settings.useCookies)
  65.             javaClientBuilder.cookieHandler(cookieManager)

  66.         if (executor != null)
  67.             javaClientBuilder.executor(executor)

  68.         settings.sslSettings?.let { javaClientBuilder.sslContext(sslContext(it)) }

  69.         javaClient = javaClientBuilder.build()

  70.         started = true
  71.     }

  72.     override fun shutDown() {
  73.         started = false
  74.     }

  75.     override fun started() =
  76.         started

  77.     override fun send(request: HttpRequestPort): HttpResponsePort {
  78.         val hexagonRequest = createRequest(request)
  79.         val javaResponse = javaClient.send(hexagonRequest, BodyHandlers.ofByteArray())
  80.         return convertResponse(javaResponse)
  81.     }

  82.     override fun ws(
  83.         path: String,
  84.         onConnect: WsSession.() -> Unit,
  85.         onBinary: WsSession.(data: ByteArray) -> Unit,
  86.         onText: WsSession.(text: String) -> Unit,
  87.         onPing: WsSession.(data: ByteArray) -> Unit,
  88.         onPong: WsSession.(data: ByteArray) -> Unit,
  89.         onClose: WsSession.(status: Int, reason: String) -> Unit,
  90.     ): WsSession {
  91.         throw UnsupportedOperationException("WebSockets not supported")
  92.     }

  93.     override fun sse(request: HttpRequestPort): Publisher<ServerEvent> {
  94.         throw UnsupportedOperationException("SSE not supported")
  95.     }

  96.     override fun supportedFeatures(): Set<HttpFeature> =
  97.         setOf(ZIP, COOKIES)

  98.     override fun supportedProtocols(): Set<HttpProtocol> =
  99.         setOf(HTTP, HTTPS, HTTP2, H2C)

  100.     private fun sslContext(sslSettings: SslSettings): SSLContext {
  101.         val sslContext = SSLContext.getInstance("TLSv1.3")

  102.         if (httpSettings.insecure)
  103.             return sslContext.apply {
  104.                 init(emptyArray(), arrayOf(TrustAll), SecureRandom.getInstanceStrong())
  105.             }

  106.         val keyManager = keyManagerFactory(sslSettings)
  107.         val trustManager = trustManagerFactory(sslSettings)
  108.         return sslContext.apply {
  109.             init(
  110.                 keyManager?.keyManagers ?: emptyArray(),
  111.                 trustManager?.trustManagers ?: emptyArray(),
  112.                 SecureRandom.getInstanceStrong()
  113.             )
  114.         }
  115.     }

  116.     private fun trustManagerFactory(sslSettings: SslSettings): TrustManagerFactory? {
  117.         val trustStoreUrl = sslSettings.trustStore ?: return null
  118.         val trustStorePassword = sslSettings.trustStorePassword
  119.         return createTrustManagerFactory(trustStoreUrl, trustStorePassword)
  120.     }

  121.     private fun keyManagerFactory(sslSettings: SslSettings): KeyManagerFactory? {
  122.         val keyStoreUrl = sslSettings.keyStore ?: return null
  123.         val keyStorePassword = sslSettings.keyStorePassword
  124.         return createKeyManagerFactory(keyStoreUrl, keyStorePassword)
  125.     }

  126.     private fun createRequest(request: HttpRequestPort): JavaHttpRequest {
  127.         val baseUri = httpSettings.baseUri

  128.         if (httpSettings.useCookies)
  129.             addCookies((baseUri ?: request.uri()), request.cookies)

  130.         val bodyBytes = bodyToBytes(request.body)
  131.         val queryParameters = request.queryParameters
  132.         val base = (baseUri?.toString() ?: "") + request.path
  133.         val uri =
  134.             if (queryParameters.isEmpty()) base
  135.             else base + '?' + formatQueryString(queryParameters)

  136.         val javaRequest = JavaHttpRequest
  137.             .newBuilder(URI(uri))
  138.             .method(request.method.toString(), BodyPublishers.ofByteArray(bodyBytes))

  139.         request.headers.all.forEach { (name, values) ->
  140.             // TODO Maybe accept-encoding interferes with H2C
  141.             if (name != "accept-encoding" && values.isNotEmpty()) {
  142.                 val kvs = values.flatMap { v -> listOf(name, v.text) }.toTypedArray()
  143.                 javaRequest.headers(*kvs)
  144.             }
  145.         }

  146.         // TODO Remove these fields and handle them as headers
  147.         request.contentType?.let { javaRequest.setHeader("content-type", it.text) }
  148.         request.authorization?.let { javaRequest.setHeader("authorization", it.text) }
  149.         request.accept.forEach { javaRequest.setHeader("accept", it.text) }

  150.         return javaRequest.build()
  151.     }

  152.     private fun addCookies(uri: URI, cookies: List<Cookie>) {
  153.         cookies.forEach {
  154.             val httpCookie = HttpCookie(it.name, it.value)
  155.             httpCookie.secure = it.secure
  156.             httpCookie.maxAge = it.maxAge
  157.             httpCookie.path = it.path
  158.             httpCookie.isHttpOnly = it.httpOnly
  159.             httpCookie.path = it.path
  160.             it.domain?.let(httpCookie::setDomain)

  161.             cookieManager.cookieStore.add(uri, httpCookie)
  162.         }
  163.     }

  164.     private fun convertResponse(response: JavaHttpResponse<ByteArray>): HttpResponse {

  165.         val bodyString = String(response.body())
  166.         val headers = response.headers()
  167.         val cookies =
  168.             if (httpSettings.useCookies)
  169.                 cookieManager.cookieStore.cookies.map {
  170.                     Cookie(
  171.                         it.name,
  172.                         it.value,
  173.                         it.maxAge,
  174.                         it.secure,
  175.                         it.path,
  176.                         it.isHttpOnly,
  177.                         it.domain,
  178.                     )
  179.                 }
  180.             else
  181.                 emptyList()

  182.         httpClient.cookies = cookies

  183.         val contentType = headers.firstValue("content-type").orElse(null)
  184.         return HttpResponse(
  185.             body = bodyString,
  186.             headers = convertHeaders(headers),
  187.             contentType = contentType?.let { parseContentType(it) },
  188.             cookies = cookies,
  189.             status = response.statusCode(),
  190.             contentLength = bodyString.length.toLong(),
  191.         )
  192.     }

  193.     private fun convertHeaders(headers: HttpHeaders): Headers =
  194.         Headers(headers.map().flatMap { (k, v) -> v.map { Header(k, it) } })
  195. }