You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
91 lines
3.6 KiB
91 lines
3.6 KiB
package dev.lowrespalmtree.comet
|
|
|
|
import android.util.Log
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.channels.Channel
|
|
import kotlinx.coroutines.channels.consumeEach
|
|
import kotlinx.coroutines.launch
|
|
import java.nio.ByteBuffer
|
|
import java.nio.charset.Charset
|
|
|
|
class Response(val code: Code, val meta: String, val data: Channel<ByteArray>) {
|
|
@Suppress("unused")
|
|
enum class Code(val value: Int) {
|
|
UNKNOWN(0),
|
|
INPUT(10),
|
|
SENSITIVE_INPUT(11),
|
|
SUCCESS(20),
|
|
REDIRECT_TEMPORARY(30),
|
|
REDIRECT_PERMANENT(31),
|
|
TEMPORARY_FAILURE(40),
|
|
SERVER_UNAVAILABLE(41),
|
|
CGI_ERROR(42),
|
|
PROXY_ERROR(43),
|
|
SLOW_DOWN(44),
|
|
PERMANENT_FAILURE(50),
|
|
NOT_FOUND(51),
|
|
GONE(52),
|
|
PROXY_REQUEST_REFUSED(53),
|
|
BAD_REQUEST(59),
|
|
CLIENT_CERTIFICATE_REQUIRED(60),
|
|
CERTIFICATE_NOT_AUTHORISED(61),
|
|
CERTIFICATE_NOT_VALID(62);
|
|
|
|
companion object {
|
|
private val MAP = values().associateBy(Code::value)
|
|
fun fromInt(type: Int) = MAP[type]
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "Response"
|
|
|
|
/** Return a response object from the incoming server data, served through the channel. */
|
|
suspend fun from(channel: Channel<ByteArray>, scope: CoroutineScope): Response? {
|
|
var received = 0
|
|
val headerBuffer = ByteBuffer.allocate(1024)
|
|
for (data in channel) {
|
|
// Push some data into our buffer.
|
|
headerBuffer.put(data)
|
|
received += data.size
|
|
// Check if there is enough data to parse a Gemini header from it (e.g. has \r\n).
|
|
val lfIndex = headerBuffer.array().indexOf(0x0D) // \r
|
|
if (lfIndex == -1)
|
|
continue
|
|
if (headerBuffer.array()[lfIndex + 1] != (0x0A.toByte())) // \n
|
|
continue
|
|
// We have our header! Parse it to create our Response object.
|
|
val bytes = headerBuffer.array()
|
|
val headerData = bytes.sliceArray(0 until lfIndex)
|
|
val (code, meta) = parseHeader(headerData)
|
|
?: return null.also { Log.e(TAG, "Failed to parse header") }
|
|
val responseChannel = Channel<ByteArray>()
|
|
val response = Response(code, meta, responseChannel)
|
|
scope.launch {
|
|
// If we got too much data from the channel: push the trailing data first.
|
|
val trailingIndex = lfIndex + 2
|
|
if (trailingIndex < received) {
|
|
val trailingData = bytes.sliceArray(trailingIndex until received)
|
|
responseChannel.send(trailingData)
|
|
}
|
|
// Forward all incoming data to the Response channel.
|
|
channel.consumeEach { responseChannel.send(it) }
|
|
}
|
|
// Return the response here; this stops consuming the channel from this for-loop so
|
|
// that the coroutine above can take care of it.
|
|
return response
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Return the code and meta from this header if it could be parsed correctly. */
|
|
private fun parseHeader(data: ByteArray): Pair<Code, String>? {
|
|
val string = data.toString(Charset.defaultCharset())
|
|
val parts = string.split(" ", limit = 2)
|
|
if (parts.size != 2)
|
|
return null
|
|
val code = parts[0].toIntOrNull()?.let { Code.fromInt(it) } ?: return null
|
|
return Pair(code, parts[1])
|
|
}
|
|
}
|
|
} |