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

235 lines
8.1 KiB
Kotlin
Raw Normal View History

2020-08-21 15:12:00 +00:00
package oppen.tva.io.gemini
2020-08-15 14:52:27 +00:00
2020-08-21 15:12:00 +00:00
import android.content.Context
import androidx.core.net.toUri
2020-08-15 14:52:27 +00:00
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
2020-08-21 15:12:00 +00:00
import oppen.tva.io.TvaState
2020-08-15 14:52:27 +00:00
import java.io.*
2020-08-18 19:02:16 +00:00
import java.net.ConnectException
import java.net.SocketException
2020-08-15 14:52:27 +00:00
import java.net.URI
import java.security.SecureRandom
2020-08-21 15:12:00 +00:00
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
2020-08-15 14:52:27 +00:00
const val GEMINI_SCHEME = "gemini"
2020-08-21 15:12:00 +00:00
class GeminiDatasource(val context: Context): Datasource {
2020-08-15 14:52:27 +00:00
2020-08-15 20:20:15 +00:00
private var last: URI? = null
2020-08-15 14:52:27 +00:00
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 -> {
2020-08-18 16:17:04 +00:00
val cached = RuntimeCache.get(uri)
if(cached != null){
last = uri
2020-08-21 15:12:00 +00:00
onUpdate(
TvaState.ResponseGemtext(
uri,
cached.first,
cached.second
)
)
2020-08-18 16:17:04 +00:00
return
}else{
onUpdate(TvaState.Requesting(uri))
2020-08-15 14:52:27 +00:00
2020-08-18 16:17:04 +00:00
GlobalScope.launch {
geminiRequest(uri, onUpdate)
}
}
2020-08-15 14:52:27 +00:00
}
2020-08-15 20:20:15 +00:00
else -> {
val address = uri.toString()
2020-08-18 16:17:04 +00:00
val parsedUri = when {
2020-08-15 20:20:15 +00:00
address.startsWith("//") -> {
//just missing protocol
2020-08-18 16:17:04 +00:00
URI.create("gemini:$address")
2020-08-15 20:20:15 +00:00
}
address.startsWith("/") -> {
//internal navigation
val internalNav = "gemini://${last?.host}$address"
2020-08-18 16:17:04 +00:00
URI.create(internalNav)
2020-08-15 20:20:15 +00:00
}
!address.contains("://") -> {
//looks like a relative link
val lastAddress = last.toString()
val relAddress = "${lastAddress.substring(0, lastAddress.lastIndexOf("/") + 1)}$address"
2020-08-18 16:17:04 +00:00
URI.create(relAddress)
}
else -> {
onUpdate(TvaState.NotGeminiRequest(uri))
2020-08-15 20:20:15 +00:00
return
}
2020-08-18 16:17:04 +00:00
}
val cached = RuntimeCache.get(parsedUri)
if(cached != null){
last = parsedUri
2020-08-21 15:12:00 +00:00
onUpdate(
TvaState.ResponseGemtext(
parsedUri,
cached.first,
cached.second
)
)
2020-08-18 16:17:04 +00:00
}else{
request(parsedUri, onUpdate)
2020-08-15 20:20:15 +00:00
}
}
2020-08-15 14:52:27 +00:00
}
}
/**
*
* 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){
2020-08-15 20:20:15 +00:00
last = uri
2020-08-15 14:52:27 +00:00
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
2020-08-18 19:02:16 +00:00
var socket: SSLSocket? = null
try {
socket = factory.createSocket(uri.host, port) as SSLSocket
socket.enabledProtocols = arrayOf("TLSv1.2")
socket.startHandshake()
}catch(ce: ConnectException){
2020-08-21 15:12:00 +00:00
onUpdate(
TvaState.ResponseError(
GeminiResponse.Header(
-1,
ce.message ?: ce.toString()
)
)
)
2020-08-18 19:02:16 +00:00
return
}catch(she: SSLHandshakeException){
2020-08-21 15:12:00 +00:00
onUpdate(
TvaState.ResponseError(
GeminiResponse.Header(
-1,
she.message ?: she.toString()
)
)
)
2020-08-18 19:02:16 +00:00
return
}
2020-08-18 19:29:16 +00:00
2020-08-15 14:52:27 +00:00
// 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()) {
2020-08-21 15:12:00 +00:00
onUpdate(
TvaState.ResponseError(
GeminiResponse.Header(
-1,
"Print Writer Error"
)
)
)
2020-08-15 14:52:27 +00:00
outWriter.close()
return
}
2020-08-18 16:17:04 +00:00
outputStreamWriter.close()
bufferedWriter.close()
outWriter.close()
2020-08-15 14:52:27 +00:00
// IN <<<<<<<<<<<<<<<<<<<<<<<<<<<
2020-08-18 16:17:04 +00:00
val inputStream = socket.inputStream
val headerInputReader = InputStreamReader(inputStream)
val bufferedReader = BufferedReader(headerInputReader)
val headerLine = bufferedReader.readLine()
2020-08-18 16:17:04 +00:00
println("Tva: header: $headerLine")
val header = GeminiResponse.parseHeader(headerLine)
when {
header.code == GeminiResponse.INPUT -> onUpdate(TvaState.ResponseInput(uri, header))
2020-08-18 19:06:08 +00:00
header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta), onUpdate)
2020-08-18 16:17:04 +00:00
header.code != GeminiResponse.SUCCESS -> onUpdate(TvaState.ResponseError(header))
header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, uri, header, onUpdate)
2020-08-18 16:17:04 +00:00
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
2020-08-21 15:12:00 +00:00
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
2020-08-21 15:59:07 +00:00
header.meta.startsWith("audio/") -> getBinary(socket, uri, header, onUpdate)
2020-08-18 16:17:04 +00:00
else -> onUpdate(TvaState.ResponseError(header))
}
bufferedReader.close()
headerInputReader.close()
socket.close()
2020-08-18 16:17:04 +00:00
}
private fun getGemtext(reader: BufferedReader, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
2020-08-18 16:17:04 +00:00
2020-08-15 14:52:27 +00:00
val lines = mutableListOf<String>()
2020-08-18 16:17:04 +00:00
lines.addAll(reader.readLines())
2020-08-17 17:42:23 +00:00
2020-08-18 16:17:04 +00:00
val processed = GemtextHelper.findCodeBlocks(lines)
RuntimeCache.put(uri, header, processed)
2020-08-18 20:21:43 +00:00
2020-08-18 16:17:04 +00:00
onUpdate(TvaState.ResponseGemtext(uri, header, processed))
}
2020-08-17 17:42:23 +00:00
2020-08-18 19:02:16 +00:00
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()
2020-08-20 17:40:54 +00:00
onUpdate(TvaState.ResponseText(uri, header, content ?: "Error fetching content"))
2020-08-15 14:52:27 +00:00
}
2020-08-21 15:12:00 +00:00
private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
val filenameRegex = Regex("[^A-Za-z0-9]")
val cacheFile = File(context.cacheDir, filenameRegex.replace(uri.path, "_"))
2020-08-21 15:59:07 +00:00
when {
cacheFile.exists() -> {
when {
header.meta.startsWith("image/") -> onUpdate(TvaState.ResponseImage(uri, header, cacheFile.toUri()))
header.meta.startsWith("audio/") -> onUpdate(TvaState.ResponseAudio(uri, header, cacheFile.toUri()))
}
2020-08-21 15:12:00 +00:00
}
2020-08-21 15:59:07 +00:00
else -> {
cacheFile.createNewFile()
cacheFile.outputStream().use{ outputStream ->
socket?.inputStream?.copyTo(outputStream)
socket?.close()
}
2020-08-21 15:12:00 +00:00
2020-08-21 15:59:07 +00:00
when {
header.meta.startsWith("image/") -> onUpdate(TvaState.ResponseImage(uri, header, cacheFile.toUri()))
header.meta.startsWith("audio/") -> onUpdate(TvaState.ResponseAudio(uri, header, cacheFile.toUri()))
}
}
2020-08-21 15:12:00 +00:00
}
}
2020-08-15 14:52:27 +00:00
}