ariane/app/src/main/java/oppen/tva/io/GeminiDatasource.kt

153 lines
5.2 KiB
Kotlin

package oppen.tva.io
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.*
import java.net.URI
import java.security.SecureRandom
import javax.net.ssl.*
const val GEMINI_SCHEME = "gemini"
class GeminiDatasource: Datasource{
private var last: URI? = null
override fun request(uri: URI, onUpdate: (state: TvaState) -> Unit) {
//Any inputted uri starting with a colon is an app-specific command, eg. :prefs :settings
if(uri.toString().startsWith(":")){
onUpdate(TvaState.AppQuery(uri))
return
}
when (uri.scheme) {
GEMINI_SCHEME -> {
val cached = RuntimeCache.get(uri)
if(cached != null){
last = uri
onUpdate(TvaState.ResponseGemtext(uri, cached.first, cached.second))
return
}else{
onUpdate(TvaState.Requesting(uri))
GlobalScope.launch {
geminiRequest(uri, onUpdate)
}
}
}
else -> {
val address = uri.toString()
val parsedUri = when {
address.startsWith("//") -> {
//just missing protocol
URI.create("gemini:$address")
}
address.startsWith("/") -> {
//internal navigation
val internalNav = "gemini://${last?.host}$address"
URI.create(internalNav)
}
!address.contains("://") -> {
//looks like a relative link
val lastAddress = last.toString()
val relAddress = "${lastAddress.substring(0, lastAddress.lastIndexOf("/") + 1)}$address"
URI.create(relAddress)
}
else -> {
onUpdate(TvaState.NotGeminiRequest(uri))
return
}
}
val cached = RuntimeCache.get(parsedUri)
if(cached != null){
last = parsedUri
onUpdate(TvaState.ResponseGemtext(parsedUri, cached.first, cached.second))
}else{
request(parsedUri, onUpdate)
}
}
}
}
/**
*
* This was largely copied from https://framagit.org/waweic/gemini-client/-/blob/master/app/src/main/java/rocks/ism/decentral/geminiclient/GeminiConnection.kt
*
*/
private fun geminiRequest(uri: URI, onUpdate: (state: TvaState) -> Unit){
last = uri
val port = if(uri.port == -1) 1965 else uri.port
val sslContext = SSLContext.getInstance("TLSv1.2")
sslContext.init(null, DummyTrustManager.get(), SecureRandom())
val factory: SSLSocketFactory = sslContext.socketFactory
val socket: SSLSocket = factory.createSocket(uri.host, port) as SSLSocket
socket.enabledProtocols = arrayOf("TLSv1.2")
socket.startHandshake()
// OUT >>>>>>>>>>>>>>>>>>>>>>>>>>
val outputStreamWriter = OutputStreamWriter(socket.outputStream)
val bufferedWriter = BufferedWriter(outputStreamWriter)
val outWriter = PrintWriter(bufferedWriter)
outWriter.print(uri.toString() + "\r\n")
outWriter.flush()
if (outWriter.checkError()) {
onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, "Print Writer Error")))
outWriter.close()
return
}
outputStreamWriter.close()
bufferedWriter.close()
outWriter.close()
// IN <<<<<<<<<<<<<<<<<<<<<<<<<<<
var headerLine = ""
InputStreamReader(socket.inputStream).use{ streamReader ->
BufferedReader(streamReader).use{ bufferedReader ->
headerLine = bufferedReader.readLine()
}
}
println("Tva: header: $headerLine")
val header = GeminiResponse.parseHeader(headerLine)
when {
header.code != GeminiResponse.SUCCESS -> onUpdate(TvaState.ResponseError(header))
header.meta == "text/gemini" -> getGemtext(socket, uri, header, onUpdate)
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
else -> onUpdate(TvaState.ResponseError(header))
}
}
private fun getGemtext(socket: SSLSocket, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
val lines = mutableListOf<String>()
socket.inputStream.reader().use { inputStreamReader ->
BufferedReader(inputStreamReader).use { reader ->
lines.addAll(reader.readLines())
}
}
socket.close()
val processed = GemtextHelper.findCodeBlocks(lines)
RuntimeCache.put(uri, header, processed)
onUpdate(TvaState.ResponseGemtext(uri, header, processed))
}
private fun getString(socket: SSLSocket, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
val content = socket.inputStream.bufferedReader().use { reader -> reader.readText() }
socket.close()
}
}