2020-09-10 21:05:11 +00:00
|
|
|
package oppen.gem.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-09-10 21:05:11 +00:00
|
|
|
import oppen.gem.io.GemState
|
2020-08-15 14:52:27 +00:00
|
|
|
import java.io.*
|
2020-08-18 19:02:16 +00:00
|
|
|
import java.net.ConnectException
|
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-09-09 18:29:23 +00:00
|
|
|
override fun request(uri: URI, onUpdate: (state: GemState) -> Unit) {
|
2020-08-15 14:52:27 +00:00
|
|
|
|
|
|
|
//Any inputted uri starting with a colon is an app-specific command, eg. :prefs :settings
|
|
|
|
if(uri.toString().startsWith(":")){
|
2020-09-09 18:29:23 +00:00
|
|
|
onUpdate(GemState.AppQuery(uri))
|
2020-08-15 14:52:27 +00:00
|
|
|
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(
|
2020-09-09 18:29:23 +00:00
|
|
|
GemState.ResponseGemtext(
|
2020-08-21 15:12:00 +00:00
|
|
|
uri,
|
|
|
|
cached.first,
|
|
|
|
cached.second
|
|
|
|
)
|
|
|
|
)
|
2020-08-18 16:17:04 +00:00
|
|
|
return
|
|
|
|
}else{
|
2020-09-09 18:29:23 +00:00
|
|
|
onUpdate(GemState.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 -> {
|
2020-09-09 18:29:23 +00:00
|
|
|
onUpdate(GemState.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(
|
2020-09-09 18:29:23 +00:00
|
|
|
GemState.ResponseGemtext(
|
2020-08-21 15:12:00 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
2020-09-09 18:29:23 +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
|
|
|
|
|
2020-08-15 14:52:27 +00:00
|
|
|
*
|
|
|
|
*/
|
2020-09-09 18:29:23 +00:00
|
|
|
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> 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-09-10 21:05:11 +00:00
|
|
|
var socket: SSLSocket?
|
2020-08-18 19:02:16 +00:00
|
|
|
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(
|
2020-09-09 18:29:23 +00:00
|
|
|
GemState.ResponseError(
|
2020-08-21 15:12:00 +00:00
|
|
|
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(
|
2020-09-09 18:29:23 +00:00
|
|
|
GemState.ResponseError(
|
2020-08-21 15:12:00 +00:00
|
|
|
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(
|
2020-09-09 18:29:23 +00:00
|
|
|
GemState.ResponseError(
|
2020-08-21 15:12:00 +00:00
|
|
|
GeminiResponse.Header(
|
|
|
|
-1,
|
|
|
|
"Print Writer Error"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2020-08-15 14:52:27 +00:00
|
|
|
outWriter.close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// IN <<<<<<<<<<<<<<<<<<<<<<<<<<<
|
2020-08-18 16:17:04 +00:00
|
|
|
|
2020-08-26 15:41:48 +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 {
|
2020-09-09 18:29:23 +00:00
|
|
|
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
|
2020-08-18 19:06:08 +00:00
|
|
|
header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta), onUpdate)
|
2020-09-09 18:29:23 +00:00
|
|
|
header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header))
|
2020-08-26 15:41:48 +00:00
|
|
|
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-09-09 18:29:23 +00:00
|
|
|
else -> onUpdate(GemState.ResponseError(header))
|
2020-08-18 16:17:04 +00:00
|
|
|
}
|
2020-08-26 15:41:48 +00:00
|
|
|
|
2020-09-09 18:29:23 +00:00
|
|
|
//Close input
|
2020-08-26 15:41:48 +00:00
|
|
|
bufferedReader.close()
|
|
|
|
headerInputReader.close()
|
2020-09-09 18:29:23 +00:00
|
|
|
|
|
|
|
//Close output:
|
|
|
|
outputStreamWriter.close()
|
|
|
|
bufferedWriter.close()
|
|
|
|
outWriter.close()
|
|
|
|
|
2020-08-26 15:41:48 +00:00
|
|
|
socket.close()
|
2020-08-18 16:17:04 +00:00
|
|
|
}
|
|
|
|
|
2020-09-09 18:29:23 +00:00
|
|
|
private fun getGemtext(reader: BufferedReader, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> 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
|
|
|
|
2020-08-26 15:41:48 +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-09-09 18:29:23 +00:00
|
|
|
onUpdate(GemState.ResponseGemtext(uri, header, processed))
|
2020-08-18 16:17:04 +00:00
|
|
|
}
|
2020-08-17 17:42:23 +00:00
|
|
|
|
2020-09-09 18:29:23 +00:00
|
|
|
private fun getString(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
|
2020-08-18 19:02:16 +00:00
|
|
|
val content = socket?.inputStream?.bufferedReader().use { reader -> reader?.readText() }
|
|
|
|
socket?.close()
|
2020-09-09 18:29:23 +00:00
|
|
|
onUpdate(GemState.ResponseText(uri, header, content ?: "Error fetching content"))
|
2020-08-15 14:52:27 +00:00
|
|
|
}
|
2020-08-21 15:12:00 +00:00
|
|
|
|
2020-09-09 18:29:23 +00:00
|
|
|
private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
|
2020-08-21 15:12:00 +00:00
|
|
|
|
|
|
|
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 {
|
2020-09-09 18:29:23 +00:00
|
|
|
header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
|
|
|
|
header.meta.startsWith("audio/") -> onUpdate(GemState.ResponseAudio(uri, header, cacheFile.toUri()))
|
2020-08-21 15:59:07 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2020-09-09 18:29:23 +00:00
|
|
|
header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
|
|
|
|
header.meta.startsWith("audio/") -> onUpdate(GemState.ResponseAudio(uri, header, cacheFile.toUri()))
|
2020-08-21 15:59:07 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-21 15:12:00 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-15 14:52:27 +00:00
|
|
|
}
|