ProjectLaogai/app/src/main/java/org/mosad/seil0/projectlaogai/controller/QISPOSParser.kt

237 lines
8.8 KiB
Kotlin

/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* 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<String, ArrayList<GradeSubject>> {
val gradesMap = HashMap<String, ArrayList<GradeSubject>>()
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<String>{
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()
//println(gradesListHtml.body())
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
}
}