Http.kt
⎇
Raw
1package de.upapp
2
3import java.io.ByteArrayOutputStream
4import java.io.DataInputStream
5import java.io.EOFException
6import java.io.IOException
7import java.io.InputStream
8import java.io.OutputStream
9
10/** [reusable] is true if the connection can carry the next request. */
11class Response(val status: Int, val headers: Map<String, String>, val body: ByteArray, val reusable: Boolean = false) {
12 val text get() = body.decodeToString()
13}
14
15/** Sends one HTTP/1.1 request and reads the response. Header names are lower case. */
16fun exchange(
17 input: InputStream, output: OutputStream, method: String, host: String, path: String, body: ByteArray? = null,
18 keepAlive: Boolean = false, extraHeaders: List<String> = emptyList(),
19): Response {
20 val head = buildString {
21 append("$method $path HTTP/1.1\r\nHost: $host\r\n")
22 if (!keepAlive) append("Connection: close\r\n")
23 extraHeaders.forEach { append("$it\r\n") }
24 if (body != null) {
25 // Same type as maps+more. Other types are untested on the car.
26 if (body.isNotEmpty()) append("Content-Type: text/plain; charset=UTF-8\r\n")
27 append("Content-Length: ${body.size}\r\n")
28 }
29 append("\r\n")
30 }
31 output.write(head.encodeToByteArray() + (body ?: ByteArray(0)))
32 output.flush()
33
34 // No buffering. Read-ahead would swallow the start of the next response on a reused connection.
35 val inp = DataInputStream(input)
36 val statusLine = inp.line()
37 val status = statusLine.split(' ').getOrNull(1)?.toIntOrNull() ?: throw IOException("Bad status line: $statusLine")
38 val headers = generateSequence { inp.line().ifEmpty { null } }
39 .associate { it.substringBefore(':').trim().lowercase() to it.substringAfter(':').trim() }
40 val length = headers["content-length"]?.toInt()
41 var framed = true
42 val bytes = when {
43 headers["transfer-encoding"]?.contains("chunked", ignoreCase = true) == true -> inp.chunked()
44 length != null -> ByteArray(length).also { inp.readFully(it) }
45 status in 100..199 || status == 204 || status == 304 -> ByteArray(0)
46 else -> inp.untilEof().also { framed = false }
47 }
48 val reusable = keepAlive && framed && !headers["connection"].equals("close", ignoreCase = true)
49 return Response(status, headers, bytes, reusable)
50}
51
52internal fun InputStream.line(): String {
53 val out = ByteArrayOutputStream()
54 while (true) {
55 val c = read()
56 if (c < 0) throw EOFException("Connection closed in HTTP header")
57 if (c == '\n'.code) break
58 out.write(c)
59 }
60 return out.toString(Charsets.ISO_8859_1.name()).trimEnd('\r')
61}
62
63private fun DataInputStream.chunked(): ByteArray {
64 val out = ByteArrayOutputStream()
65 while (true) {
66 val size = line().substringBefore(';').trim().toInt(16)
67 if (size == 0) break
68 out.write(ByteArray(size).also { readFully(it) })
69 line()
70 }
71 while (line().isNotEmpty()) Unit
72 return out.toByteArray()
73}
74
75private fun InputStream.untilEof(): ByteArray {
76 val out = ByteArrayOutputStream()
77 val buf = ByteArray(4096)
78 try {
79 while (true) {
80 val n = read(buf)
81 if (n < 0) break
82 out.write(buf, 0, n)
83 }
84 } catch (_: EOFException) {
85 // A peer that closes without TLS close_notify still ends the body.
86 }
87 return out.toByteArray()
88}
89