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) {
|
||||
val uri = Uri.parse(url)
|
||||
if (uri.scheme != "gemini") {
|
||||
alertLiveData.postValue("Unknown scheme.")
|
||||
alertLiveData.postValue("Can't process scheme \"${uri.scheme}\".")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val request = Request(uri)
|
||||
val socket = request.connect()
|
||||
val channel = request.proceed(socket, this)
|
||||
val charset = Charset.defaultCharset()
|
||||
for (data in channel) {
|
||||
val decoded = charset.decode(ByteBuffer.wrap(data)).toString()
|
||||
source += decoded
|
||||
sourceLiveData.postValue(source)
|
||||
val response = Response.from(channel, viewModelScope)
|
||||
if (response == null) {
|
||||
alertLiveData.postValue("Can't parse server response.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
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 ->
|
||||
BufferedInputStream(socket_input_stream).use { bis ->
|
||||
try {
|
||||
@Suppress("BlockingMethodInNonBlockingContext") // what u gonna do
|
||||
while ((bis.read(buffer).also { numRead = it }) >= 0) {
|
||||
Log.d(TAG, "proceed coroutine: received $numRead bytes")
|
||||
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>
|
||||
<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="url">URL</string>
|
||||
</resources>
|
Reference in a new issue