package de.upapp import java.io.ByteArrayOutputStream import java.io.DataInputStream import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream /** [reusable] is true if the connection can carry the next request. */ class Response(val status: Int, val headers: Map, val body: ByteArray, val reusable: Boolean = false) { val text get() = body.decodeToString() } /** Sends one HTTP/1.1 request and reads the response. Header names are lower case. */ fun exchange( input: InputStream, output: OutputStream, method: String, host: String, path: String, body: ByteArray? = null, keepAlive: Boolean = false, extraHeaders: List = emptyList(), ): Response { val head = buildString { append("$method $path HTTP/1.1\r\nHost: $host\r\n") if (!keepAlive) append("Connection: close\r\n") extraHeaders.forEach { append("$it\r\n") } if (body != null) { // Same type as maps+more. Other types are untested on the car. if (body.isNotEmpty()) append("Content-Type: text/plain; charset=UTF-8\r\n") append("Content-Length: ${body.size}\r\n") } append("\r\n") } output.write(head.encodeToByteArray() + (body ?: ByteArray(0))) output.flush() // No buffering. Read-ahead would swallow the start of the next response on a reused connection. val inp = DataInputStream(input) val statusLine = inp.line() val status = statusLine.split(' ').getOrNull(1)?.toIntOrNull() ?: throw IOException("Bad status line: $statusLine") val headers = generateSequence { inp.line().ifEmpty { null } } .associate { it.substringBefore(':').trim().lowercase() to it.substringAfter(':').trim() } val length = headers["content-length"]?.toInt() var framed = true val bytes = when { headers["transfer-encoding"]?.contains("chunked", ignoreCase = true) == true -> inp.chunked() length != null -> ByteArray(length).also { inp.readFully(it) } status in 100..199 || status == 204 || status == 304 -> ByteArray(0) else -> inp.untilEof().also { framed = false } } val reusable = keepAlive && framed && !headers["connection"].equals("close", ignoreCase = true) return Response(status, headers, bytes, reusable) } internal fun InputStream.line(): String { val out = ByteArrayOutputStream() while (true) { val c = read() if (c < 0) throw EOFException("Connection closed in HTTP header") if (c == '\n'.code) break out.write(c) } return out.toString(Charsets.ISO_8859_1.name()).trimEnd('\r') } private fun DataInputStream.chunked(): ByteArray { val out = ByteArrayOutputStream() while (true) { val size = line().substringBefore(';').trim().toInt(16) if (size == 0) break out.write(ByteArray(size).also { readFully(it) }) line() } while (line().isNotEmpty()) Unit return out.toByteArray() } private fun InputStream.untilEof(): ByteArray { val out = ByteArrayOutputStream() val buf = ByteArray(4096) try { while (true) { val n = read(buf) if (n < 0) break out.write(buf, 0, n) } } catch (_: EOFException) { // A peer that closes without TLS close_notify still ends the body. } return out.toByteArray() }