parse response headers
This commit is contained in:
parent
cbcd0dca73
commit
8e161dbb31
26
ARCHITECTURE.md
Normal file
26
ARCHITECTURE.md
Normal 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.
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
91
app/src/main/java/dev/lowrespalmtree/comet/Response.kt
Normal file
91
app/src/main/java/dev/lowrespalmtree/comet/Response.kt
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
Reference in a new issue