/** * ProjectLaogai * * Copyright 2019-2020 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * */ package org.mosad.seil0.projectlaogai.controller import android.content.Context import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.jsoup.HttpStatusException import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.mosad.seil0.projectlaogai.R import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences import org.mosad.seil0.projectlaogai.util.GradeSubject import java.security.KeyStore import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.* import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import kotlin.collections.ArrayList import kotlin.collections.HashMap /** * Parse the qispos site the get all needed data for the grades fragment */ class QISPOSParser(val context: Context) { private val className = this.javaClass.name private val baseURL = "https://notenverwaltung.hs-offenburg.de" private val loginPath = "/qispos/rds?state=user&type=1&category=auth.login&startpage=portal.vm&breadCrumbSource=portal" /** * check if qispos is available * @return a http status code, 999 if no HttpStatusException supplied */ fun checkQISPOSStatus(): Int { return runBlocking { withContext(Dispatchers.IO) { val socketFactory = createSSLSocketFactory() try { val res = Jsoup.connect(baseURL + loginPath) .sslSocketFactory(socketFactory) .followRedirects(true) .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0") .execute() res.statusCode() } catch (exHttp: HttpStatusException) { exHttp.statusCode } catch (ex: Exception) { Log.e(className, "Error while checking status", ex) 999 } } } } /** * parse the html from readGrades() * @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects */ fun parseGrades(): SortedMap> { val gradesMap = HashMap>() val gradesListHtml = readGrades() gradesListHtml?.select("table > tbody > tr")?.forEach { val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright") // only real subjects will be selected if(row.size >= 6 && row[0].text().length >=7) { val subject = GradeSubject( id = row[0].text(), name = row[1].text(), semester = row[2].text(), grade = if (row[3].text().isNotEmpty()) row[3].text() else row[4].text(), credits = row[5].text() ) if (gradesMap.containsKey(subject.semester)) { gradesMap[subject.semester]!!.add(subject) } else { gradesMap[subject.semester] = arrayListOf(subject) } } } // return the sorted map return gradesMap.toSortedMap(compareBy{ val oText = it.substringAfter(" ") if (oText.contains("/")) { oText.substringBefore("/").toInt() + 0.5 } else { oText.toDouble() } }.thenBy { it }) } /** * read the grades html from qispos * @return the grades list as html element or null */ private fun readGrades(): Element?{ val credentials = EncryptedPreferences.readCredentials(context) val username = credentials.first.substringBefore("@") val password = credentials.second return runBlocking { withContext(Dispatchers.IO) { try { val socketFactory = createSSLSocketFactory() // login, asdf = username, fdsa = password, wtf val list = mapOf( Pair("asdf", username), Pair("fdsa", password), Pair("submit", "Anmelden") ) // login and get session cookies val res = Jsoup.connect(baseURL + loginPath) .sslSocketFactory(socketFactory) .followRedirects(true) .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0") .data(list) .postDataCharset("UTF-8") .execute() Log.i(className, "login status is: ${res.statusMessage()}: ${res.statusCode()}") val loginCookies = res.cookies() // grades root document and url val rootHtml =Jsoup.parse(res.body()) val gradesRootLink = rootHtml.select("li.menueListStyle > a.auflistung").last().attr("abs:href") // parse grades url val gradesHtml = Jsoup.connect(gradesRootLink) .sslSocketFactory(socketFactory) .followRedirects(true) .cookies(loginCookies) .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0") .get() val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href") val gradesNextHtml = Jsoup.connect(gradesNextLink) .sslSocketFactory(socketFactory) .followRedirects(true) .cookies(loginCookies) .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0") .get() val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li").selectFirst("a").attr("abs:href") // get the grades list val gradesListHtml = Jsoup.connect(gradesListLink) .sslSocketFactory(socketFactory) .followRedirects(true) .cookies(loginCookies) .referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0") .get() gradesListHtml.body() } catch (ex: Exception) { Log.e(className, "error while loading qispos", ex) null } } } } /** * since the HS has a fucked up tls setup we need to work around that */ private fun createSSLSocketFactory(): SSLSocketFactory { // Load CAs from an InputStream // (could be from a resource or ByteArrayInputStream or ...) val cf: CertificateFactory = CertificateFactory.getInstance("X.509") val caInput = context.resources.openRawResource(R.raw.notenverwaltung_hs_offenburg_de) val ca = caInput.use { cf.generateCertificate(it) as X509Certificate } // Create a KeyStore containing our trusted CAs val keyStoreType = KeyStore.getDefaultType() val keyStore = KeyStore.getInstance(keyStoreType).apply { load(null, null) setCertificateEntry("ca", ca) } // Create a TrustManager that trusts the CAs inputStream our KeyStore val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm() val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply { init(keyStore) } // Create an SSLContext that uses our TrustManager val sslContext = SSLContext.getInstance("TLS").apply { init(null, tmf.trustManagers, null) } return sslContext.socketFactory } }