Skip to content

Commit

Permalink
refactor: Rewrite wallet functionality to mach app code style
Browse files Browse the repository at this point in the history
  • Loading branch information
Lastaapps committed Oct 25, 2023
1 parent e8d66e0 commit 1806db7
Show file tree
Hide file tree
Showing 49 changed files with 1,833 additions and 600 deletions.
7 changes: 4 additions & 3 deletions api/agata/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ dependencies {
commonMainImplementation(projects.core)
commonMainImplementation(projects.api.core)

// skrape-it
// androidMainImplementation(projects.htmlParser)
androidMainImplementation("it.skrape:skrapeit:1.2.2")

commonMainImplementation(libs.ktor.client.core)
commonMainImplementation(libs.ktor.client.contentNegotiation)
commonMainImplementation(libs.ktor.client.logging)
commonMainImplementation(libs.ktor.client.serialization)

commonMainImplementation(projects.entity)
commonMainImplementation(projects.htmlParser)

commonMainImplementation(libs.bundles.russhwolf.settings)
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2023, Petr Laštovička as Lasta apps, All rights reserved
*
* This file is part of Menza.
*
* Menza 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.
*
* Menza 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 Menza. If not, see <https://www.gnu.org/licenses/>.
*/

package cz.lastaapps.menza.api.agata.api

import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.flatten
import arrow.core.left
import arrow.core.raise.nullable
import arrow.core.right
import cz.lastaapps.core.domain.Outcome
import cz.lastaapps.core.domain.error.ApiError.WalletError
import cz.lastaapps.core.util.extensions.catchingNetwork
import io.ktor.client.HttpClient
import io.ktor.client.plugins.BrowserUserAgent
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.Cookie
import io.ktor.http.HttpHeaders
import io.ktor.http.Parameters
import it.skrape.core.htmlDocument
import java.net.URLDecoder

internal class AndroidAgataCtuWalletApi(
httpClient: HttpClient,
) : AgataCtuWalletApi {

private val client: HttpClient

init {
client = httpClient.config {
// Disable redirects, because when I was testing it with Python I got into loop sometimes
// Also it has to extract some cookies from some of the requests, so redirects manually handled
followRedirects = false
// Because of many redirects
expectSuccess = false

BrowserUserAgent()
}
}

// Get balance from Agata
// Originally written by Marekkon5
override suspend fun getBalance(
username: String,
password: String,
): Outcome<Float> = catchingNetwork {
nullable {
// Go to the auth provider
client.get("https://agata.suz.cvut.cz/secure/index.php")
.headers[HttpHeaders.Location]
.bind()
.let { url -> client.get(url) }

// Get new url params
.let { request ->
val body = request.bodyAsText()
val returnUrl = Regex("var returnURL = \"(.+?)\"")
.find(body)?.groups?.get(1)?.value.bind()
val otherParams = Regex("var otherParams = \"(.+?)\"")
.find(body)?.groups?.get(1)?.value.bind()
"${returnUrl}&entityID=https://idp2.civ.cvut.cz/idp/shibboleth${otherParams}"
}
// Get to SSO
.let { url -> client.get(url).headers[HttpHeaders.Location].bind() }
.let { sso -> client.get(sso) }
.let { ssoResponse ->
// Extract JSESSIONID cookie
val jsessionid = ssoResponse.headers[HttpHeaders.SetCookie]
?.split(";")
?.firstOrNull()
?.split("=")
?.getOrNull(1)
.bind()

// Resolve SSO
val url =
"https://idp2.civ.cvut.cz${ssoResponse.headers[HttpHeaders.Location]}"

// result ignored, must happen
client.get(url)

client.post(url) {
setBody(
FormDataContent(
Parameters.build {
append("j_username", username)
append("j_password", password)
append("_eventId_proceed", "")
},
),
)
Cookie("JSESSIONID", jsessionid)
header(HttpHeaders.Referrer, url)
header(HttpHeaders.ContentType, "application/x-www-form-urlencoded")
}
}

// Extract response codes from html
.let { htmlDocument(it.bodyAsText()) }
.let { html ->
var relayState: String? = null
var samlResponse: String? = null

val inputField = Either.catch {
html.findAll("input")
}
when (inputField) {
is Left -> return@catchingNetwork WalletError.InvalidCredentials.left()
is Right -> inputField.value
}.forEach {
if (it.attribute("name") == "RelayState") {
relayState = URLDecoder.decode(it.attribute("value"), "UTF-8")
}
if (it.attribute("name") == "SAMLResponse") {
samlResponse = it.attribute("value")
}
}

// Send the shit back to Agata and get session cookie
val response =
client.post("https://agata.suz.cvut.cz/Shibboleth.sso/SAML2/POST") {
setBody(
FormDataContent(
Parameters.build {
append("RelayState", relayState.bind())
append("SAMLResponse", samlResponse.bind())
},
),
)
header(HttpHeaders.Referrer, "https://idp2.civ.cvut.cz/")
}

response.headers[HttpHeaders.SetCookie]
?.split(";")
?.getOrNull(0)
.bind()
}.let { sessionCookie ->
// Get balance from Agata
client.get("https://agata.suz.cvut.cz/secure/index.php") {
// The session cookie has variable name, so using raw headers here
header("Cookie", sessionCookie)
}
}.let { finalResponse ->
val html = htmlDocument(finalResponse.bodyAsText())

// Parse
html.findFirst("h4 span.badge").text
.lowercase()
.replace("", "")
.replace(",", ".")
.replace(" ", "")
.trim()
.toFloatOrNull()
.bind()
}
}?.right() ?: WalletError.TotallyBroken.left()
}.flatten()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@

package cz.lastaapps.menza.api.agata.di

import cz.lastaapps.menza.api.agata.api.AgataCtuWalletApi
import cz.lastaapps.menza.api.agata.api.AndroidAgataCtuWalletApi
import cz.lastaapps.menza.api.agata.data.createAgataDBDriver
import org.koin.core.module.Module
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.bind
import org.koin.dsl.module

internal actual val platform: Module = module {
factory { createAgataDBDriver(get()) }
factoryOf(::AndroidAgataCtuWalletApi) bind AgataCtuWalletApi::class
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
package cz.lastaapps.menza.api.agata.api

import cz.lastaapps.core.domain.Outcome
import io.ktor.client.HttpClient

expect class AgataWallet(httpClient: HttpClient) {
interface WalletApi {
suspend fun getBalance(username: String, password: String): Outcome<Float>
}
}

interface AgataCtuWalletApi : WalletApi
3 changes: 3 additions & 0 deletions api/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ dependencies {
commonMainImplementation(libs.kotlinx.atomicfu)
commonMainImplementation(libs.sqldelight.runtime)
commonMainImplementation(libs.bundles.russhwolf.settings)

androidMainImplementation(libs.androidx.security)
androidMainImplementation(libs.androidx.datastore)
}
Loading

0 comments on commit 1806db7

Please sign in to comment.