All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.apollographql.apollo.mockserver.http.kt Maven / Gradle / Ivy

package com.apollographql.apollo.mockserver

import okio.Buffer

internal interface Reader {
  val buffer: Buffer
  suspend fun fillBuffer()
}

internal typealias WriteData = (ByteArray) -> Unit

private fun parseHeader(line: String): Pair {
  val index = line.indexOfFirst { it == ':' }
  check(index >= 0) {
    "Invalid header: $line"
  }

  return line.substring(0, index).trim() to line.substring(index + 1, line.length).trim()
}


private fun parseRequestLine(line: String): Triple {
  val regex = Regex("([A-Z-a-z]*) ([^ ]*) (.*)")
  val match = regex.matchEntire(line)
  check(match != null) {
    "Cannot match request line: $line"
  }

  val method = match.groupValues[1].uppercase()
  check(method in listOf("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH")) {
    "Unknown method $method"
  }

  return Triple(method, match.groupValues[2], match.groupValues[3])
}

internal class ConnectionClosed(cause: Throwable?) : Exception("client closed the connection", cause)

internal suspend fun readRequest(reader: Reader): MockRequestBase {
  suspend fun nextLine(): String {
    while (true) {
      val newline = reader.buffer.indexOf('\n'.code.toByte())
      if (newline != -1L) {
        return reader.buffer.readUtf8(newline + 1)
      } else {
        reader.fillBuffer()
      }
    }
  }

  suspend fun readBytes(size: Long): Buffer {
    val buffer2 = Buffer()
    while (buffer2.size < size) {
      if (reader.buffer.size == 0L) {
        reader.fillBuffer()
      }

      buffer2.write(reader.buffer, minOf(size, reader.buffer.size))
    }

    return buffer2
  }

  var line = try {
    nextLine()
  } catch (e: Exception) {
    /**
     * XXX: if the connection is closed in the middle of the first request line, this is detected
     * as a normal connection close.
     */
    throw ConnectionClosed(e)
  }

  val (method, path, version) = parseRequestLine(line.trimEol())
  //println("Line: ${line.trimEol()}")

  val headers = mutableMapOf()

  /**
   * Read headers
   */
  while (true) {
    line = nextLine()
    //println("Headers: ${line.trimEol()}")
    if (line == "\r\n") {
      break
    }

    val (key, value) = parseHeader(line.trimEol())
    headers.put(key, value)
  }

  val contentLength = headers.headerValueOf("content-length")?.toLongOrNull() ?: 0
  val transferEncoding = headers.headerValueOf("transfer-encoding")?.lowercase()
  check(transferEncoding == null || transferEncoding == "identity" || transferEncoding == "chunked") {
    "Transfer-Encoding $transferEncoding is not supported"
  }

  val body = when {
    headers.get("Upgrade") == "websocket" -> null
    contentLength > 0 -> readBytes(contentLength)
    transferEncoding == "chunked" -> {
      val buffer2 = Buffer()
      /**
       * Read a source encoded in the "Transfer-Encoding: chunked" encoding.
       * This format is a sequence of:
       * - chunk-size (in hexadecimal) + CRLF
       * - chunk-data + CRLF
       */
      while (true) {
        val chunkSize = nextLine().trimEol().toLong(16)
        if (chunkSize == 0L) {
          check(nextLine() == "\r\n") // CRLF
          break
        } else {
          buffer2.writeAll(readBytes(chunkSize))
          check(nextLine() == "\r\n") // CRLF
        }
      }
      buffer2
    }

    else -> Buffer()
  }


  return if (body != null) {
    MockRequest(
        method = method,
        path = path,
        version = version,
        headers = headers,
        body = body.readByteString()
    )
  } else {
    WebsocketMockRequest(
        method = method,
        path = path,
        version = version,
        headers = headers,
    )
  }
}

private fun String.trimEol() = this.trimEnd('\r', '\n')

internal suspend fun writeResponse(response: MockResponse, version: String, writeData: WriteData) {
  writeData("$version ${response.statusCode}\r\n".encodeToByteArray())

  val headers = response.headers
  headers.forEach {
    writeData("${it.key}: ${it.value}\r\n".encodeToByteArray())
  }
  writeData("\r\n".encodeToByteArray())

  response.body.collect {
    // XXX: flow control
    writeData(it.toByteArray())
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy