JavaClientAdapter.kt

  1. package com.hexagonkt.http.client.java

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

  38. /**
  39.  * Client to use other REST services.
  40.  */
  41. class JavaClientAdapter(
  42.     private val protocol: HttpProtocol = HTTP2,
  43.     private val executor: Executor? = null,
  44. ) : HttpClientPort {

  45.     private companion object {
  46.         object TrustAll : X509TrustManager {
  47.             override fun getAcceptedIssuers(): Array<X509Certificate> =
  48.                 emptyArray()

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

  50.             override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
  51.         }
  52.     }

  53.     private lateinit var javaClient: JavaHttpClient
  54.     private lateinit var httpClient: HttpClient
  55.     private lateinit var httpSettings: HttpClientSettings
  56.     private var started: Boolean = false
  57.     private val cookieManager: CookieManager by lazy { CookieManager() }

  58.     override fun startUp(client: HttpClient) {
  59.         val settings = client.settings

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

  66.         if (settings.useCookies)
  67.             javaClientBuilder.cookieHandler(cookieManager)

  68.         if (executor != null)
  69.             javaClientBuilder.executor(executor)

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

  71.         javaClient = javaClientBuilder.build()

  72.         started = true
  73.     }

  74.     override fun shutDown() {
  75.         started = false
  76.     }

  77.     override fun started() =
  78.         started

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

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

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

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

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

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

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

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

  124.     private fun createRequest(request: HttpRequestPort): JavaHttpRequest {
  125.         val baseUrl = httpSettings.baseUrl

  126.         if (httpSettings.useCookies)
  127.             addCookies((baseUrl ?: request.url()).toURI(), request.cookies)

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

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

  137.         request.headers.forEach { h ->
  138.             val name = h.value.name
  139.             val values = h.value.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.toString()) }.toTypedArray()
  143.                 javaRequest.headers(*kvs)
  144.             }
  145.         }

  146.         request.contentType?.let { javaRequest.setHeader("content-type", it.text) }
  147.         request.authorization?.let { javaRequest.setHeader("authorization", it.text) }
  148.         request.accept.forEach { javaRequest.setHeader("accept", it.text) }

  149.         return javaRequest.build()
  150.     }

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

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

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

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

  181.         httpClient.cookies = cookies

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

  192.     private fun convertHeaders(headers: HttpHeaders): Headers =
  193.         Headers(
  194.             headers
  195.                 .map()
  196.                 .filter { it.key !in CHECKED_HEADERS }
  197.                 .map { Header(it.key, it.value) }
  198.         )
  199. }