JettyClientAdapter.kt

  1. package com.hexagonkt.http.client.jetty

  2. import com.hexagonkt.core.media.TEXT_EVENT_STREAM
  3. import com.hexagonkt.core.security.loadKeyStore
  4. import com.hexagonkt.http.handlers.bodyToBytes
  5. import com.hexagonkt.http.CHECKED_HEADERS
  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.model.HttpResponse
  10. import com.hexagonkt.http.model.*
  11. import com.hexagonkt.http.model.CookieSameSite.*
  12. import com.hexagonkt.http.model.ws.WsSession
  13. import com.hexagonkt.http.parseContentType
  14. import org.eclipse.jetty.client.HttpResponseException
  15. import org.eclipse.jetty.client.ContentResponse
  16. import org.eclipse.jetty.client.Request
  17. import org.eclipse.jetty.client.Response
  18. import org.eclipse.jetty.client.transport.HttpClientTransportDynamic
  19. import org.eclipse.jetty.client.transport.HttpClientConnectionFactory.HTTP11
  20. import org.eclipse.jetty.client.BytesRequestContent
  21. import org.eclipse.jetty.client.MultiPartRequestContent
  22. import org.eclipse.jetty.client.StringRequestContent
  23. import org.eclipse.jetty.http.HttpCookie
  24. import org.eclipse.jetty.http.HttpCookie.SameSite
  25. import org.eclipse.jetty.http.HttpCookieStore
  26. import org.eclipse.jetty.http.HttpFields
  27. import org.eclipse.jetty.http.HttpFields.EMPTY
  28. import org.eclipse.jetty.http.HttpMethod
  29. import org.eclipse.jetty.http.MultiPart.ContentSourcePart
  30. import org.eclipse.jetty.io.ClientConnector
  31. import java.lang.StringBuilder
  32. import java.lang.UnsupportedOperationException
  33. import java.net.URI
  34. import java.util.concurrent.ExecutionException
  35. import java.util.concurrent.Executors
  36. import java.util.concurrent.Flow.Publisher
  37. import java.util.concurrent.SubmissionPublisher
  38. import org.eclipse.jetty.http2.client.HTTP2Client as JettyHttp2Client
  39. import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2.HTTP2
  40. import org.eclipse.jetty.client.HttpClient as JettyHttpClient
  41. import org.eclipse.jetty.util.ssl.SslContextFactory.Client as ClientSslContextFactory

  42. /**
  43.  * Client to use other REST services.
  44.  */
  45. open class JettyClientAdapter : HttpClientPort {

  46.     protected lateinit var jettyClient: JettyHttpClient
  47.     protected lateinit var httpClient: HttpClient
  48.     private lateinit var httpSettings: HttpClientSettings
  49.     private var started: Boolean = false
  50.     private val publisherExecutor = Executors.newSingleThreadExecutor()

  51.     override fun startUp(client: HttpClient) {
  52.         val clientConnector = ClientConnector()
  53.         val settings = client.settings
  54.         clientConnector.sslContextFactory = sslContext(settings)

  55.         val http2 = HTTP2(JettyHttp2Client(clientConnector))
  56.         val transport = HttpClientTransportDynamic(clientConnector, HTTP11, http2)

  57.         jettyClient = JettyHttpClient(transport)
  58.         httpClient = client
  59.         httpSettings = settings

  60.         jettyClient.userAgentField = null // Disable default user agent header
  61.         jettyClient.isFollowRedirects = settings.followRedirects
  62.         jettyClient.start()
  63.         started = true
  64.     }

  65.     override fun shutDown() {
  66.         jettyClient.stop()
  67.         started = false
  68.     }

  69.     override fun started() =
  70.         started

  71.     override fun send(request: HttpRequestPort): HttpResponsePort {
  72.         val response =
  73.             try {
  74.                 createJettyRequest(jettyClient, request).send()
  75.             }
  76.             catch (e: ExecutionException) {
  77.                 val cause = e.cause
  78.                 if (cause is HttpResponseException) cause.response
  79.                 else throw e
  80.             }

  81.         return convertJettyResponse(httpClient, jettyClient, response)
  82.     }

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

  94.     override fun sse(request: HttpRequestPort): Publisher<ServerEvent> {
  95.         val clientPublisher = SubmissionPublisher<ServerEvent>(publisherExecutor, Int.MAX_VALUE)

  96.         val sseRequest = request.with(
  97.             accept = listOf(ContentType(TEXT_EVENT_STREAM)),
  98.             headers = request.headers + Header("connection", "keep-alive")
  99.         )

  100.         createJettyRequest(jettyClient, sseRequest)
  101.             .onResponseBegin {
  102.                 if (it.status !in 200 until 300)
  103.                     error("Invalid response: ${it.status}")
  104.             }
  105.             .onResponseContent { _, content ->
  106.                 val sb = StringBuilder()
  107.                 while (content.hasRemaining())
  108.                     sb.append(Char(content.get().toInt()))

  109.                 val evt = sb
  110.                     .trim()
  111.                     .lines()
  112.                     .map { it.split(":") }
  113.                     .associate { it.first().trim().lowercase() to it.last().trim() }
  114.                     .let { ServerEvent(it["event"], it["data"], it["id"], it["retry"]?.toLong()) }

  115.                 clientPublisher.submit(evt)
  116.             }
  117.             .send {
  118.                 clientPublisher.close()
  119.             }

  120.         return clientPublisher
  121.     }

  122.     private fun convertJettyResponse(
  123.         adapterHttpClient: HttpClient, adapterJettyClient: JettyHttpClient, response: Response
  124.     ): HttpResponse {

  125.         val bodyString = if (response is ContentResponse) response.contentAsString else ""

  126.         if (httpSettings.useCookies)
  127.             adapterHttpClient.cookies = adapterJettyClient.httpCookieStore.all().map {
  128.                 Cookie(
  129.                     it.name,
  130.                     it.value,
  131.                     it.maxAge,
  132.                     it.isSecure,
  133.                     it.path,
  134.                     it.isHttpOnly,
  135.                     it.domain,
  136.                     it.attributes["SameSite"]?.uppercase()?.let(CookieSameSite::valueOf),
  137.                     expires = it.expires,
  138.                 )
  139.             }

  140.         return HttpResponse(
  141.             body = bodyString,
  142.             headers = convertHeaders(response.headers),
  143.             contentType = response.headers["content-type"]?.let { parseContentType(it) },
  144.             cookies = adapterHttpClient.cookies,
  145.             status = HttpStatus(response.status),
  146.             contentLength = bodyString.length.toLong(),
  147.         )
  148.     }

  149.     private fun convertHeaders(headers: HttpFields): Headers =
  150.         Headers(
  151.             headers
  152.                 .fieldNamesCollection
  153.                 .map { it.lowercase() }
  154.                 .filter { it !in CHECKED_HEADERS }
  155.                 .map { Header(it, headers.getValuesList(it)) }
  156.         )

  157.     private fun createJettyRequest(
  158.         adapterJettyClient: JettyHttpClient, request: HttpRequestPort
  159.     ): Request {

  160.         val contentType = request.contentType
  161.         val authorization = request.authorization
  162.         val baseUrl = httpSettings.baseUrl

  163.         if (httpSettings.useCookies) {
  164.             val uri = (baseUrl ?: request.url()).toURI()
  165.             addCookies(uri, adapterJettyClient.httpCookieStore, request.cookies)
  166.         }

  167.         val jettyRequest = adapterJettyClient
  168.             .newRequest(URI((baseUrl?.toString() ?: "") + request.path))
  169.             .method(HttpMethod.valueOf(request.method.toString()))
  170.             .headers {
  171.                 it.remove("accept-encoding") // Don't send encoding by default
  172.                 if (contentType != null)
  173.                     it.put("content-type", contentType.text)
  174.                 if (authorization != null)
  175.                     it.put("authorization", authorization.text)
  176.                 request.headers.values.forEach { (k, v) ->
  177.                     v.map(Any::toString).forEach { s -> it.add(k, s)}
  178.                 }
  179.             }
  180.             .body(createBody(request))
  181.             .accept(*request.accept.map { it.text }.toTypedArray())

  182.         request.queryParameters
  183.             .forEach { (k, v) -> v.strings().forEach { jettyRequest.param(k, it) } }

  184.         return jettyRequest
  185.     }

  186.     private fun createBody(request: HttpRequestPort): Request.Content {

  187.         if (request.parts.isEmpty() && request.formParameters.isEmpty())
  188.             return BytesRequestContent(bodyToBytes(request.body))

  189.         val multiPart = MultiPartRequestContent()

  190.         request.parts.forEach { p ->
  191.             if (p.submittedFileName == null)
  192.                 // TODO Add content type if present
  193.                 multiPart.addPart(
  194.                     ContentSourcePart(p.name, null, EMPTY, StringRequestContent(p.bodyString()))
  195.                 )
  196.             else
  197.                 multiPart.addPart(
  198.                     ContentSourcePart(
  199.                         p.name,
  200.                         p.submittedFileName,
  201.                         EMPTY,
  202.                         BytesRequestContent(bodyToBytes(p.body)),
  203.                     )
  204.                 )
  205.         }

  206.         request.formParameters
  207.             .forEach { (k, v) ->
  208.                 v.strings().forEach {
  209.                     multiPart.addPart(ContentSourcePart(k, null, EMPTY, StringRequestContent(it)))
  210.                 }
  211.             }

  212.         multiPart.close()

  213.         return multiPart
  214.     }

  215.     private fun addCookies(uri: URI, store: HttpCookieStore, cookies: List<Cookie>) {
  216.         cookies.forEach {
  217.             val httpCookie = java.net.HttpCookie(it.name, it.value)
  218.             httpCookie.secure = it.secure
  219.             httpCookie.maxAge = it.maxAge
  220.             httpCookie.path = it.path
  221.             httpCookie.isHttpOnly = it.httpOnly
  222.             it.domain?.let(httpCookie::setDomain)

  223.             val from = HttpCookie.build(httpCookie).expires(it.expires)

  224.             it.sameSite?.let { ss ->
  225.                 when(ss){
  226.                     STRICT -> SameSite.STRICT
  227.                     LAX -> SameSite.LAX
  228.                     NONE -> SameSite.NONE
  229.                 }
  230.             }?.let { ss -> from.sameSite(ss) }

  231.             store.add(uri, from.build())
  232.         }
  233.     }

  234.     private fun sslContext(settings: HttpClientSettings): ClientSslContextFactory =
  235.         when {
  236.             settings.insecure ->
  237.                 ClientSslContextFactory().apply { isTrustAll = true }

  238.             settings.sslSettings != null -> {
  239.                 val sslSettings = settings.sslSettings ?: error("SSL settings cannot be 'null'")
  240.                 val keyStore = sslSettings.keyStore
  241.                 val trustStore = sslSettings.trustStore
  242.                 val sslContextBuilder = ClientSslContextFactory()

  243.                 if (keyStore != null) {
  244.                     val store = loadKeyStore(keyStore, sslSettings.keyStorePassword)
  245.                     sslContextBuilder.keyStore = store
  246.                     sslContextBuilder.keyStorePassword = sslSettings.keyStorePassword
  247.                 }

  248.                 if (trustStore != null) {
  249.                     val store = loadKeyStore(trustStore, sslSettings.trustStorePassword)
  250.                     sslContextBuilder.trustStore = store
  251.                     sslContextBuilder.setTrustStorePassword(sslSettings.trustStorePassword)
  252.                 }

  253.                 sslContextBuilder
  254.             }

  255.             else ->
  256.                 ClientSslContextFactory()
  257.         }
  258. }