package oppen.tva.io import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.io.* import java.net.ConnectException 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 var socket: SSLSocket? = null try { socket = factory.createSocket(uri.host, port) as SSLSocket socket.enabledProtocols = arrayOf("TLSv1.2") socket.startHandshake() }catch(ce: ConnectException){ onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString()))) return }catch(she: SSLHandshakeException){ onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, she.message ?: she.toString()))) return } // 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.INPUT -> onUpdate(TvaState.ResponseInput(uri, header)) header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta), onUpdate) header.code != GeminiResponse.SUCCESS -> onUpdate(TvaState.ResponseError(header)) header.meta.startsWith("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() 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() onUpdate(TvaState.ResponseText(uri, header, content ?: "Error fetching content")) } }