parse response headers

This commit is contained in:
dece 2021-12-05 20:03:52 +01:00
parent cbcd0dca73
commit 8e161dbb31
5 changed files with 140 additions and 7 deletions

26
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,26 @@
Architecture
============
Streaming
---------
Just because I'm frustrated by how hard it is to implement streaming on Bebop
the way I've built it so far, properly streaming responses is a core focus of
Comet, even though I barely encounter streamed pages on Gemini.
This is the streaming process I want to do, not sure if it makes sense.
- `connect/proceed` block and read from the server.
- Gemtext parts
- Views are created from Gemtext parts (or a single TextView for text/* files).
We need some kind of producer/consumer here.
- The page view is a vertical LinearLayout (should we use a cursed Recycler?).
New views should be passed to the activity and added at the end of the
layout (does it work without blinking or other issues?).
The data is received through a buffered SSLSocket object.
1. Receive data
2. If we can parse a header, do it.

View file

@ -66,18 +66,33 @@ class MainActivity : AppCompatActivity() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
if (uri.scheme != "gemini") { if (uri.scheme != "gemini") {
alertLiveData.postValue("Unknown scheme.") alertLiveData.postValue("Can't process scheme \"${uri.scheme}\".")
return@launch return@launch
} }
val request = Request(uri) val request = Request(uri)
val socket = request.connect() val socket = request.connect()
val channel = request.proceed(socket, this) val channel = request.proceed(socket, this)
val charset = Charset.defaultCharset() val response = Response.from(channel, viewModelScope)
for (data in channel) { if (response == null) {
val decoded = charset.decode(ByteBuffer.wrap(data)).toString() alertLiveData.postValue("Can't parse server response.")
source += decoded return@launch
sourceLiveData.postValue(source)
} }
Log.i(TAG, "sendRequest: got ${response.code} with meta \"${response.meta}\"")
when (response.code) {
Response.Code.SUCCESS -> handleRequestSuccess(response)
else -> alertLiveData.postValue("Can't handle code ${response.code}.")
}
}
}
private suspend fun handleRequestSuccess(response: Response) {
val charset = Charset.defaultCharset()
for (data in response.data) {
val decoded = charset.decode(ByteBuffer.wrap(data)).toString()
source += decoded
sourceLiveData.postValue(source)
} }
} }
} }

View file

@ -37,6 +37,7 @@ class Request(private val uri: Uri) {
socket.inputStream.use { socket_input_stream -> socket.inputStream.use { socket_input_stream ->
BufferedInputStream(socket_input_stream).use { bis -> BufferedInputStream(socket_input_stream).use { bis ->
try { try {
@Suppress("BlockingMethodInNonBlockingContext") // what u gonna do
while ((bis.read(buffer).also { numRead = it }) >= 0) { while ((bis.read(buffer).also { numRead = it }) >= 0) {
Log.d(TAG, "proceed coroutine: received $numRead bytes") Log.d(TAG, "proceed coroutine: received $numRead bytes")
val received = buffer.sliceArray(0 until numRead) val received = buffer.sliceArray(0 until numRead)

View file

@ -0,0 +1,91 @@
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])
}
}
}

View file

@ -1,6 +1,6 @@
<resources> <resources>
<string name="app_name">Comet</string> <string name="app_name">Comet</string>
<string name="alert_title"></string> <string name="alert_title">Alert</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="url">URL</string> <string name="url">URL</string>
</resources> </resources>