You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
8.7 KiB
236 lines
8.7 KiB
/** |
|
* 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() |
|
|
|
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 |
|
} |
|
|
|
|
|
|
|
|
|
} |