Http.kt

  1. package com.hexagonkt.http

  2. import com.hexagonkt.core.GMT_ZONE
  3. import com.hexagonkt.core.assertEnabled
  4. import com.hexagonkt.core.Jvm
  5. import com.hexagonkt.core.media.MediaType
  6. import com.hexagonkt.core.text.encodeToBase64
  7. import com.hexagonkt.http.model.*
  8. import java.net.URLDecoder
  9. import java.net.URLEncoder
  10. import java.nio.charset.Charset
  11. import java.time.*
  12. import java.time.ZoneOffset.UTC
  13. import java.time.format.DateTimeFormatter
  14. import java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME

  15. /** Headers handled by HTTP model as headers with special meaning. */
  16. val CHECKED_HEADERS: List<String> by lazy {
  17.     listOf("content-type", "accept", "set-cookie", "authorization")
  18. }

  19. internal val HTTP_DATE_FORMATTER: DateTimeFormatter by lazy { RFC_1123_DATE_TIME.withZone(UTC) }

  20. fun basicAuth(user: String, password: String = ""): String =
  21.     "$user:$password".encodeToBase64()

  22. fun checkHeaders(headers: Headers) {
  23.     if (!assertEnabled)
  24.         return

  25.     val headersKeys = headers.httpFields.keys
  26.     val invalidHeaders = CHECKED_HEADERS.filter { headersKeys.contains(it) }

  27.     check(invalidHeaders.isEmpty()) {
  28.         val invalidHeadersText = invalidHeaders.joinToString(",") { "'$it'" }

  29.         """
  30.         Special headers should be handled with their respective properties (i.e.: contentType)
  31.         instead setting them in the headers map. Ignored headers: $invalidHeadersText
  32.         """.trimIndent()
  33.     }
  34. }

  35. /**
  36.  * Parse query string such as `paramA=valueA&paramB=valueB` into a map of several key-value pairs
  37.  * separated by '&' where *key* is the param name before '=' as String and *value* is the string
  38.  * after '=' as a list of String (as a query parameter may have many values).
  39.  *
  40.  * Note: Missing the '=' sign, or missing value after '=' (e.g. `foo=` or `foo`) will result into an
  41.  * empty string value.
  42.  *
  43.  * @param query URL query string. E.g.: `param=value&foo=bar`.
  44.  * @return Map with query parameter keys bound to a list with their values.
  45.  *
  46.  */
  47. fun parseQueryString(query: String): QueryParameters =
  48.     if (query.isBlank())
  49.         QueryParameters()
  50.     else
  51.         QueryParameters(
  52.             query
  53.                 .split("&".toRegex())
  54.                 .map {
  55.                     val keyValue = it.split("=").map(String::trim)
  56.                     val key = keyValue[0]
  57.                     val value = if (keyValue.size == 2) keyValue[1] else ""
  58.                     key.urlDecode() to value.urlDecode()
  59.                 }
  60.                 .filter { it.first.isNotBlank() }
  61.                 .groupBy { it.first }
  62.                 .mapValues { pair -> pair.value.map { it.second } }
  63.                 .map { (k, v) -> QueryParameter(k, v) }
  64.         )

  65. fun formatQueryString(parameters: QueryParameters): String =
  66.     parameters
  67.         .flatMap { (k, v) -> v.strings().map { k to it } }
  68.         .filter { it.first.isNotBlank() }
  69.         .joinToString("&") { (k, v) ->
  70.             if (v.isBlank()) k.urlEncode()
  71.             else "${k.urlEncode()}=${v.urlEncode()}"
  72.         }

  73. fun String.urlDecode(): String =
  74.     URLDecoder.decode(this, Jvm.charset)

  75. fun String.urlEncode(): String =
  76.     URLEncoder.encode(this, Jvm.charset)

  77. fun LocalDateTime.toHttpFormat(): String =
  78.     HTTP_DATE_FORMATTER.format(ZonedDateTime.of(this, Jvm.zoneId).withZoneSameInstant(GMT_ZONE))

  79. fun Instant.toHttpFormat(): String =
  80.     HTTP_DATE_FORMATTER.format(this)

  81. fun parseContentType(contentType: String): ContentType {
  82.     val typeParameter = contentType.split(";")
  83.     val fullType = typeParameter.first().trim()
  84.     val mimeType = MediaType(fullType)

  85.     return when (typeParameter.size) {
  86.         1 -> ContentType(mimeType)
  87.         2 -> {
  88.             val parameter = typeParameter.last()
  89.             val nameValue = parameter.split("=")
  90.             if (nameValue.size != 2)
  91.                 error("Invalid content type format: $contentType")

  92.             val name = nameValue.first().trim()
  93.             val value = nameValue.last().trim()

  94.             when (name.trim().lowercase()) {
  95.                 "boundary" -> ContentType(mimeType, boundary = value)
  96.                 "charset" -> ContentType(mimeType, charset = Charset.forName(value))
  97.                 "q" -> ContentType(mimeType, q = value.toDouble())
  98.                 else -> error("Invalid content type format: $contentType")
  99.             }
  100.         }
  101.         else -> error("Invalid content type format: $contentType")
  102.     }
  103. }