JettyHttpClient.kt

  1. package com.hexagontk.http.client.jetty

  2. import com.hexagontk.core.media.TEXT_EVENT_STREAM
  3. import com.hexagontk.core.security.loadKeyStore
  4. import com.hexagontk.http.handlers.bodyToBytes
  5. import com.hexagontk.http.HttpFeature
  6. import com.hexagontk.http.HttpFeature.*
  7. import com.hexagontk.http.client.HttpClient
  8. import com.hexagontk.http.client.HttpClientPort
  9. import com.hexagontk.http.client.HttpClientSettings
  10. import com.hexagontk.http.model.HttpResponse
  11. import com.hexagontk.http.model.*
  12. import com.hexagontk.http.model.CookieSameSite.*
  13. import com.hexagontk.http.model.HttpProtocol.HTTP
  14. import com.hexagontk.http.model.HttpProtocol.HTTPS
  15. import com.hexagontk.http.model.ws.WsSession
  16. import com.hexagontk.http.parseContentType
  17. import org.eclipse.jetty.client.HttpResponseException
  18. import org.eclipse.jetty.client.ContentResponse
  19. import org.eclipse.jetty.client.Request
  20. import org.eclipse.jetty.client.Response
  21. import org.eclipse.jetty.client.transport.HttpClientTransportDynamic
  22. import org.eclipse.jetty.client.transport.HttpClientConnectionFactory.HTTP11
  23. import org.eclipse.jetty.client.BytesRequestContent
  24. import org.eclipse.jetty.client.MultiPartRequestContent
  25. import org.eclipse.jetty.client.StringRequestContent
  26. import org.eclipse.jetty.http.HttpCookie
  27. import org.eclipse.jetty.http.HttpCookie.SameSite
  28. import org.eclipse.jetty.http.HttpCookieStore
  29. import org.eclipse.jetty.http.HttpFields as JettyHttpFields
  30. import org.eclipse.jetty.http.HttpFields.EMPTY
  31. import org.eclipse.jetty.http.HttpMethod
  32. import org.eclipse.jetty.http.MultiPart.ContentSourcePart
  33. import org.eclipse.jetty.io.ClientConnector
  34. import java.net.URI
  35. import java.util.concurrent.ExecutionException
  36. import java.util.concurrent.Executors
  37. import java.util.concurrent.Flow.Publisher
  38. import java.util.concurrent.SubmissionPublisher
  39. import org.eclipse.jetty.http2.client.HTTP2Client as JettyHttp2Client
  40. import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2.HTTP2
  41. import org.eclipse.jetty.client.HttpClient as JettyClient
  42. import org.eclipse.jetty.util.ssl.SslContextFactory.Client as ClientSslContextFactory

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

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

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

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

  58.         jettyClient = JettyClient(transport)
  59.         httpClient = client
  60.         httpSettings = settings

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

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

  70.     override fun started() =
  71.         started

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

  82.         return convertJettyResponse(httpClient, jettyClient, response)
  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. Use 'http_client_jetty_ws")
  94.     }

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

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

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

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

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

  121.         return clientPublisher
  122.     }

  123.     override fun supportedFeatures(): Set<HttpFeature> =
  124.         setOf(ZIP, COOKIES, MULTIPART, SSE)

  125.     override fun supportedProtocols(): Set<HttpProtocol> =
  126.         setOf(HTTP, HTTPS, HttpProtocol.HTTP2)

  127.     private fun convertJettyResponse(
  128.         adapterHttpClient: HttpClient, adapterJettyClient: JettyClient, response: Response
  129.     ): HttpResponse {

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

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

  145.         return HttpResponse(
  146.             body = bodyString,
  147.             headers = convertHeaders(response.headers),
  148.             contentType = response.headers["content-type"]?.let { parseContentType(it) },
  149.             cookies = adapterHttpClient.cookies,
  150.             status = response.status,
  151.             contentLength = bodyString.length.toLong(),
  152.         )
  153.     }

  154.     private fun convertHeaders(headers: JettyHttpFields): Headers =
  155.         Headers(
  156.             headers
  157.                 .fieldNamesCollection
  158.                 .map { it.lowercase() }
  159.                 .flatMap { h -> headers.getValuesList(h).map { Header(h, it) } }
  160.         )

  161.     private fun createJettyRequest(
  162.         adapterJettyClient: JettyClient, request: HttpRequestPort
  163.     ): Request {

  164.         // TODO Remove these fields and handle them as headers
  165.         val contentType = request.contentType
  166.         val authorization = request.authorization
  167.         val baseUri = httpSettings.baseUri

  168.         if (httpSettings.useCookies) {
  169.             val uri = baseUri ?: request.uri()
  170.             addCookies(uri, adapterJettyClient.httpCookieStore, request.cookies)
  171.         }

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

  187.         request.queryParameters.all
  188.             .forEach { (k, v) -> v.forEach { jettyRequest.param(k, it.text) } }

  189.         return jettyRequest
  190.     }

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

  192.         if (request.parts.isEmpty() && request.formParameters.isEmpty())
  193.             return BytesRequestContent(bodyToBytes(request.body))

  194.         val multiPart = MultiPartRequestContent()

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

  211.         request.formParameters.fields.forEach {
  212.             val part = ContentSourcePart(it.name, null, EMPTY, StringRequestContent(it.text))
  213.             multiPart.addPart(part)
  214.         }

  215.         multiPart.close()

  216.         return multiPart
  217.     }

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

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

  227.             it.sameSite?.let { ss ->
  228.                 when(ss){
  229.                     STRICT -> SameSite.STRICT
  230.                     LAX -> SameSite.LAX
  231.                     NONE -> SameSite.NONE
  232.                 }
  233.             }?.let { ss -> from.sameSite(ss) }

  234.             store.add(uri, from.build())
  235.         }
  236.     }

  237.     private fun sslContext(settings: HttpClientSettings): ClientSslContextFactory =
  238.         when {
  239.             settings.insecure ->
  240.                 ClientSslContextFactory().apply { isTrustAll = true }

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

  246.                 if (keyStore != null) {
  247.                     val store = loadKeyStore(keyStore, sslSettings.keyStorePassword)
  248.                     sslContextBuilder.keyStore = store
  249.                     sslContextBuilder.keyStorePassword = sslSettings.keyStorePassword
  250.                 }

  251.                 if (trustStore != null) {
  252.                     val store = loadKeyStore(trustStore, sslSettings.trustStorePassword)
  253.                     sslContextBuilder.trustStore = store
  254.                     sslContextBuilder.setTrustStorePassword(sslSettings.trustStorePassword)
  255.                 }

  256.                 sslContextBuilder
  257.             }

  258.             else ->
  259.                 ClientSslContextFactory()
  260.         }
  261. }